From 7adc0952771ac62a0e13a60ad9855050b2be3272 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 31 Jul 2014 09:57:57 +0200 Subject: [PATCH] [WO-515] Fix folder handling Use the first folder provided in well known folders Add migration script to fix clients with multiple folders for same category Handle folder deletion smarter Extract some magic number/strings to constants in emaildao --- package.json | 2 +- src/js/app-config.js | 3 +- src/js/controller/mail-list.js | 116 ++++------------- src/js/dao/email-dao.js | 180 +++++++++++++++++---------- src/js/util/update/update-handler.js | 5 +- src/js/util/update/update-v5.js | 56 +++++++++ test/unit/email-dao-test.js | 79 ++++++++++-- test/unit/update-handler-test.js | 124 +++++++++++++++++- 8 files changed, 394 insertions(+), 171 deletions(-) create mode 100644 src/js/util/update/update-v5.js diff --git a/package.json b/package.json index b95ccf6..42cedda 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "crypto-lib": "~0.2.1", - "imap-client": "~0.3.7", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-515", "mailreader": "~0.3.5", "pgpmailer": "~0.3.11", "pgpbuilder": "~0.3.7", diff --git a/src/js/app-config.js b/src/js/app-config.js index 84722f4..1694a96 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -92,9 +92,10 @@ define(function(require) { iconPath: '/img/icon.png', verificationUrl: '/verify/', verificationUuidLength: 36, - dbVersion: 4, + dbVersion: 5, appVersion: appVersion, outboxMailboxPath: 'OUTBOX', + outboxMailboxName: 'Outbox', outboxMailboxType: 'Outbox' }; diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 503d25b..2fbcb83 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -533,92 +533,25 @@ define(function(require) { this.cc = [{ address: 'john.doe@gmail.com' }]; // list of receivers - if (attachments) { - // body structure with three attachments - this.bodystructure = { - "1": { - "part": "1", - "type": "text/plain", - "parameters": { - "charset": "us-ascii" - }, - "encoding": "7bit", - "size": 9, - "lines": 2 - }, - "2": { - "part": "2", - "type": "application/octet-stream", - "parameters": { - "name": "a.md" - }, - "encoding": "7bit", - "size": 123, - "disposition": [{ - "type": "attachment", - "filename": "a.md" - }] - }, - "3": { - "part": "3", - "type": "application/octet-stream", - "parameters": { - "name": "b.md" - }, - "encoding": "7bit", - "size": 456, - "disposition": [{ - "type": "attachment", - "filename": "b.md" - }] - }, - "4": { - "part": "4", - "type": "application/octet-stream", - "parameters": { - "name": "c.md" - }, - "encoding": "7bit", - "size": 789, - "disposition": [{ - "type": "attachment", - "filename": "c.md" - }] - }, - "type": "multipart/mixed" - }; - this.attachments = [{ - "filename": "a.md", - "filesize": 123, - "mimeType": "text/x-markdown", - "part": "2", - "content": null - }, { - "filename": "b.md", - "filesize": 456, - "mimeType": "text/x-markdown", - "part": "3", - "content": null - }, { - "filename": "c.md", - "filesize": 789, - "mimeType": "text/x-markdown", - "part": "4", - "content": null - }]; - } else { - this.bodystructure = { - "part": "1", - "type": "text/plain", - "parameters": { - "charset": "us-ascii" - }, - "encoding": "7bit", - "size": 9, - "lines": 2 - }; - this.attachments = []; - } + this.attachments = attachments ? [{ + "filename": "a.md", + "filesize": 123, + "mimeType": "text/x-markdown", + "part": "2", + "content": null + }, { + "filename": "b.md", + "filesize": 456, + "mimeType": "text/x-markdown", + "part": "3", + "content": null + }, { + "filename": "c.md", + "filesize": 789, + "mimeType": "text/x-markdown", + "part": "4", + "content": null + }] : []; this.unread = unread; this.answered = answered; this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)'); @@ -639,13 +572,14 @@ define(function(require) { this.decrypted = true; }; - var dummys = [new Email(true, true), new Email(true, false, true), new Email(false, true, true), new Email(false, true)]; - - for (var i = 0; i < 100; i++) { - dummys.push(new Email(false)); + var dummies = [], + i = 100; + while (i--) { + // every second/third/fourth dummy mail with unread/attachments/answered + dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0))); } - return dummys; + return dummies; } return MailListCtrl; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 294a39a..fa000d9 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -6,6 +6,38 @@ define(function(require) { config = require('js/app-config').config, str = require('js/app-config').string; + + // + // + // Constants + // + // + + var FOLDER_DB_TYPE = 'folders'; + + var SYNC_TYPE_NEW = 'new'; + var SYNC_TYPE_DELETED = 'deleted'; + var SYNC_TYPE_MSGS = 'messages'; + + var FOLDER_TYPE_INBOX = 'Inbox'; + var FOLDER_TYPE_SENT = 'Sent'; + var FOLDER_TYPE_DRAFTS = 'Drafts'; + var FOLDER_TYPE_TRASH = 'Trash'; + + var MSG_ATTR_UID = 'uid'; + var MSG_PART_ATTR_CONTENT = 'content'; + var MSG_PART_TYPE_ATTACHMENT = 'attachment'; + var MSG_PART_TYPE_ENCRYPTED = 'encrypted'; + var MSG_PART_TYPE_SIGNED = 'signed'; + var MSG_PART_TYPE_TEXT = 'text'; + var MSG_PART_TYPE_HTML = 'html'; + + // + // + // Email Dao + // + // + /** * High-level data access object that orchestrates everything around the handling of encrypted mails: * PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence @@ -253,8 +285,8 @@ define(function(require) { return; } - var storedUids = _.pluck(storedMessages, 'uid'), - memoryUids = _.pluck(folder.messages, 'uid'), + var storedUids = _.pluck(storedMessages, MSG_ATTR_UID), + memoryUids = _.pluck(folder.messages, MSG_ATTR_UID), newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk @@ -370,7 +402,7 @@ define(function(require) { // this enables us to already show the attachment clip in the message list ui messages.forEach(function(message) { message.attachments = message.bodyParts.filter(function(bodyPart) { - return bodyPart.type === 'attachment'; + return bodyPart.type === MSG_PART_TYPE_ATTACHMENT; }); }); @@ -401,7 +433,7 @@ define(function(require) { return; } - var body = _.pluck(filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n'), + var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'), verificationUrlPrefix = config.cloudUrl + config.verificationUrl, uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength), uuidRegex = /[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}/; @@ -645,10 +677,10 @@ define(function(require) { // we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html) // but we spare the effort and fetch attachment content later upon explicit user request. var contentParts = localMessage.bodyParts.filter(function(bodyPart) { - return bodyPart.type !== "attachment" || (bodyPart.type === "attachment" && bodyPart.id); + return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id); }); var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) { - return bodyPart.type === "attachment" && !bodyPart.id; + return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id; }); // do we need to fetch content from the imap server? @@ -700,7 +732,7 @@ define(function(require) { function extractContent() { if (message.encrypted) { // show the encrypted message - message.body = filterBodyParts(message.bodyParts, 'encrypted')[0].content; + message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content; return done(); } @@ -708,13 +740,13 @@ define(function(require) { if (message.signed) { // PGP/MIME signed - var signedRoot = filterBodyParts(message.bodyParts, 'signed')[0]; // in case of a signed message, you only want to show the signed content and ignore the rest + var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest message.signedMessage = signedRoot.signedMessage; message.signature = signedRoot.signature; root = signedRoot.content; } - var body = _.pluck(filterBodyParts(root, 'text'), 'content').join('\n'); + var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); /* * if the message is plain text and contains pgp/inline, we are only interested in the encrypted @@ -730,7 +762,7 @@ define(function(require) { // replace the bodyParts info with an artificial bodyPart of type "encrypted" message.bodyParts = [{ - type: 'encrypted', + type: MSG_PART_TYPE_ENCRYPTED, content: pgpInlineMatch[0], _isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader }]; @@ -770,8 +802,8 @@ define(function(require) { function setBody() { message.body = body; if (!message.clearSignedMessage) { - message.attachments = filterBodyParts(root, 'attachment'); - message.html = _.pluck(filterBodyParts(root, 'html'), 'content').join('\n'); + message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT); + message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); inlineExternalImages(message); } @@ -863,7 +895,7 @@ define(function(require) { } // get the receiver's public key to check the message signature - var encryptedNode = filterBodyParts(message.bodyParts, 'encrypted')[0]; + var encryptedNode = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0]; var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) { if (err || !decrypted) { @@ -897,7 +929,7 @@ define(function(require) { if (!message.signed) { // message had no signature in the ciphertext, so there's a little extra effort to be done here // is there a signed MIME node? - var signedRoot = filterBodyParts(root, 'signed')[0]; + var signedRoot = filterBodyParts(root, MSG_PART_TYPE_SIGNED)[0]; if (!signedRoot) { // no signed MIME node, obviously an unsigned PGP/MIME message return setBody(); @@ -927,9 +959,9 @@ define(function(require) { function setBody() { // we have successfully interpreted the descrypted message, // so let's update the views on the message parts - message.body = _.pluck(filterBodyParts(root, 'text'), 'content').join('\n'); - message.html = _.pluck(filterBodyParts(root, 'html'), 'content').join('\n'); - message.attachments = _.reject(filterBodyParts(root, 'attachment'), function(attmt) { + message.body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); + message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); + message.attachments = _.reject(filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT), function(attmt) { // remove the pgp-signature from the attachments return attmt.mimeType === "application/pgp-signature"; }); @@ -989,7 +1021,7 @@ define(function(require) { // upload the sent message to the sent folder if necessary var sentFolder = _.findWhere(self._account.folders, { - type: 'Sent' + type: FOLDER_TYPE_SENT }); if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { @@ -1039,7 +1071,7 @@ define(function(require) { // upload the sent message to the sent folder if necessary var sentFolder = _.findWhere(self._account.folders, { - type: 'Sent' + type: FOLDER_TYPE_SENT }); if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { @@ -1130,7 +1162,7 @@ define(function(require) { var uids, highestModseq, lastUid; - uids = _.pluck(folder.messages, 'uid').sort(function(a, b) { + uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) { return a - b; }); lastUid = uids[uids.length - 1]; @@ -1153,7 +1185,7 @@ define(function(require) { // set up the imap client to listen for changes in the inbox var inbox = _.findWhere(self._account.folders, { - type: 'Inbox' + type: FOLDER_TYPE_INBOX }); if (!inbox) { @@ -1199,14 +1231,14 @@ define(function(require) { return; } - if (options.type === 'new') { + if (options.type === SYNC_TYPE_NEW) { // new messages available on imap, fetch from imap and store to disk and memory self.fetchMessages({ folder: folder, firstUid: Math.min.apply(null, options.list), lastUid: Math.max.apply(null, options.list) }, self.onError.bind(self)); - } else if (options.type === 'deleted') { + } else if (options.type === SYNC_TYPE_DELETED) { // messages have been deleted, remove from local storage and memory options.list.forEach(function(uid) { var message = _.findWhere(folder.messages, { @@ -1223,7 +1255,7 @@ define(function(require) { localOnly: true }, self.onError.bind(self)); }); - } else if (options.type === 'messages') { + } else if (options.type === SYNC_TYPE_MSGS) { // NB! several possible reasons why this could be called. // if a message in the array has uid value and flag array, it had a possible flag update options.list.forEach(function(changedMsg) { @@ -1268,13 +1300,12 @@ define(function(require) { * @param {Function} callback Invoked when the folders are up to date */ EmailDAO.prototype._initFoldersFromDisk = function(callback) { - var self = this, - folderDbType = 'folders'; + var self = this; self.busy(); // start the spinner // fetch list from local cache - self._devicestorage.listItems(folderDbType, 0, null, function(err, stored) { + self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { if (err) { return done(err); } @@ -1297,8 +1328,7 @@ define(function(require) { * @param {Function} callback Invoked when the folders are up to date */ EmailDAO.prototype._initFoldersFromImap = function(callback) { - var self = this, - folderDbType = 'folders'; + var self = this; self.busy(); // start the spinner @@ -1308,57 +1338,73 @@ define(function(require) { return done(err); } - // this array is dropped directly into the ui to create the folder list - var folders = []; - if (wellKnownFolders.inbox) { - folders.push(wellKnownFolders.inbox); - } - if (wellKnownFolders.sent) { - folders.push(wellKnownFolders.sent); - } - folders.push({ - type: 'Outbox', + // initialize the folders to something meaningful if that hasn't already happened + self._account.folders = self._account.folders || []; + + // smuggle the outbox into the well known folders, which is obv not present on imap... + wellKnownFolders[config.outboxMailboxType] = [{ + name: config.outboxMailboxName, + type: config.outboxMailboxType, path: config.outboxMailboxPath - }); - if (wellKnownFolders.drafts) { - folders.push(wellKnownFolders.drafts); - } - if (wellKnownFolders.trash) { - folders.push(wellKnownFolders.trash); - } + }]; - var foldersChanged = false; // indicates if are there any new/removed folders? + // indicates if we need to persist anything to disk + var foldersChanged = false; - // check for added folders - folders.forEach(function(folder) { - if (!_.findWhere(self._account.folders, { - path: folder.path - })) { - // add the missing folder - self._account.folders.push(folder); + // the folders listed in the navigation pane + [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, config.outboxMailboxType, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { + var localFolderWithType, imapFolderWithPath; + + // check if there is a folder of this type locally available + localFolderWithType = _.findWhere(self._account.folders, { + type: mbxType + }); + + if (localFolderWithType) { + // we have a local folder available, so let's check if this folder still exists on imap + + imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], { + path: localFolderWithType.path + }); + + if (imapFolderWithPath) { + // folder present on imap, no need to update. + return; + } + + // folder not present on imap, so remove the folder and see if there are any updates for this folder type + self._account.folders.splice(self._account.folders.indexOf(localFolderWithType), 1); foldersChanged = true; } - }); - // check for deleted folders - self._account.folders.forEach(function(folder) { - if (!_.findWhere(folders, { - path: folder.path - })) { - // remove the obsolete folder - self._account.folders.splice(self._account.folders.indexOf(folder), 1); - foldersChanged = true; + if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) { + // no imap folders of the respective mailbox type, so nothing to do here + return; } + + /** + * we have no local folder of the type, so do something intelligent, + * i.e. take the first folder of the respective type + */ + self._account.folders.push(wellKnownFolders[mbxType][0]); + foldersChanged = true; }); - // if folder have changed, we need to persist them to disk. + // if folders have not changed, can fill them with messages directly if (!foldersChanged) { return self._initMessagesFromDisk(done); } // persist encrypted list in device storage - // NB! persis the array we received from IMAP! do *not* persist self._account.folders with all the messages... - self._devicestorage.storeList([folders], folderDbType, function(err) { + // note: the folders in the ui also include the messages array, so let's create a clean array here + var folders = self._account.folders.map(function(folder) { + return { + name: folder.name, + path: folder.path, + type: folder.type + }; + }); + self._devicestorage.storeList([folders], FOLDER_DB_TYPE, function(err) { if (err) { return done(err); } @@ -1463,7 +1509,7 @@ define(function(require) { } var trash = _.findWhere(this._account.folders, { - type: 'Trash' + type: FOLDER_TYPE_TRASH }); // there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message diff --git a/src/js/util/update/update-handler.js b/src/js/util/update/update-handler.js index 3c5f043..a622128 100644 --- a/src/js/util/update/update-handler.js +++ b/src/js/util/update/update-handler.js @@ -5,7 +5,8 @@ define(function(require) { updateV1 = require('js/util/update/update-v1'), updateV2 = require('js/util/update/update-v2'), updateV3 = require('js/util/update/update-v3'), - updateV4 = require('js/util/update/update-v4'); + updateV4 = require('js/util/update/update-v4'), + updateV5 = require('js/util/update/update-v5'); /** * Handles database migration @@ -13,7 +14,7 @@ define(function(require) { var UpdateHandler = function(appConfigStorage, userStorage, auth) { this._appConfigStorage = appConfigStorage; this._userStorage = userStorage; - this._updateScripts = [updateV1, updateV2, updateV3, updateV4]; + this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5]; this._auth = auth; }; diff --git a/src/js/util/update/update-v5.js b/src/js/util/update/update-v5.js new file mode 100644 index 0000000..ef3a035 --- /dev/null +++ b/src/js/util/update/update-v5.js @@ -0,0 +1,56 @@ +define(function() { + 'use strict'; + + var FOLDER_TYPE_INBOX = 'Inbox'; + var FOLDER_TYPE_SENT = 'Sent'; + var FOLDER_TYPE_DRAFTS = 'Drafts'; + var FOLDER_TYPE_TRASH = 'Trash'; + + var FOLDER_DB_TYPE = 'folders'; + var VERSION_DB_TYPE = 'dbVersion'; + + var POST_UPDATE_DB_VERSION = 5; + + /** + * Update handler for transition database version 4 -> 5 + * + * Due to an overlooked issue, there may be multiple folders, e.g. for sent mails. + * This removes the "duplicate" folders. + */ + function update(options, callback) { + + // remove the emails + options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + if (err) { + return callback(err); + } + + var folders = stored[0] || []; + [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { + var foldersForType = folders.filter(function(mbx) { + return mbx.type === mbxType; + }); + + if (foldersForType.length <= 1) { + return; // nothing to do here + } + + // remove duplicate folders + for (var i = 1; i < foldersForType.length; i++) { + folders.splice(folders.indexOf(foldersForType[i]), 1); + } + }); + + options.userStorage.storeList([folders], FOLDER_DB_TYPE, function(err) { + if (err) { + return callback(err); + } + + // update the database version to POST_UPDATE_DB_VERSION + options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); + }); + }); + } + + return update; +}); \ No newline at end of file diff --git a/test/unit/email-dao-test.js b/test/unit/email-dao-test.js index 728e4ac..a49ab79 100644 --- a/test/unit/email-dao-test.js +++ b/test/unit/email-dao-test.js @@ -40,30 +40,35 @@ define(function(require) { asymKeySize = 2048; inboxFolder = { + name: 'Inbox', type: 'Inbox', path: 'INBOX', messages: [] }; sentFolder = { + name: 'Sent', type: 'Sent', path: 'SENT', messages: [] }; draftsFolder = { + name: 'Drafts', type: 'Drafts', path: 'DRAFTS', messages: [] }; outboxFolder = { + name: 'Outbox', type: 'Outbox', path: 'OUTBOX', messages: [] }; trashFolder = { + name: 'Trash', type: 'Trash', path: 'TRASH', messages: [] @@ -1969,18 +1974,76 @@ define(function(require) { it('should initialize from imap if online', function(done) { account.folders = []; imapClientStub.listWellKnownFolders.yieldsAsync(null, { - inbox: inboxFolder, - sent: sentFolder, - drafts: draftsFolder, - trash: trashFolder + Inbox: [inboxFolder], + Sent: [sentFolder], + Drafts: [draftsFolder], + Trash: [trashFolder] }); devicestorageStub.storeList.withArgs(sinon.match(function(arg) { - expect(arg[0][0]).to.deep.equal(inboxFolder); - expect(arg[0][1]).to.deep.equal(sentFolder); + expect(arg[0][0].name).to.deep.equal(inboxFolder.name); + expect(arg[0][0].path).to.deep.equal(inboxFolder.path); + expect(arg[0][0].type).to.deep.equal(inboxFolder.type); + expect(arg[0][1].name).to.deep.equal(sentFolder.name); + expect(arg[0][1].path).to.deep.equal(sentFolder.path); + expect(arg[0][1].type).to.deep.equal(sentFolder.type); + expect(arg[0][2].name).to.deep.equal(outboxFolder.name); expect(arg[0][2].path).to.deep.equal(outboxFolder.path); expect(arg[0][2].type).to.deep.equal(outboxFolder.type); - expect(arg[0][3]).to.deep.equal(draftsFolder); - expect(arg[0][4]).to.deep.equal(trashFolder); + expect(arg[0][3].name).to.deep.equal(draftsFolder.name); + expect(arg[0][3].path).to.deep.equal(draftsFolder.path); + expect(arg[0][3].type).to.deep.equal(draftsFolder.type); + expect(arg[0][4].name).to.deep.equal(trashFolder.name); + expect(arg[0][4].path).to.deep.equal(trashFolder.path); + expect(arg[0][4].type).to.deep.equal(trashFolder.type); + return true; + }), 'folders').yieldsAsync(); + + dao.refreshFolder.yieldsAsync(); + + dao._initFoldersFromImap(function(err) { + expect(err).to.not.exist; + expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + done(); + }); + }); + + it('should update folders from imap', function(done) { + account.folders = [inboxFolder, outboxFolder, trashFolder, { + name: 'foo', + type: 'Sent', + path: 'bar', + }]; + + imapClientStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [inboxFolder], + Sent: [sentFolder], + Drafts: [draftsFolder], + Trash: [trashFolder] + }); + devicestorageStub.storeList.withArgs(sinon.match(function(arg) { + expect(arg[0]).to.deep.equal([{ + name: inboxFolder.name, + path: inboxFolder.path, + type: inboxFolder.type + }, { + name: outboxFolder.name, + path: outboxFolder.path, + type: outboxFolder.type + }, { + name: trashFolder.name, + path: trashFolder.path, + type: trashFolder.type + }, { + name: sentFolder.name, + path: sentFolder.path, + type: sentFolder.type + }, { + name: draftsFolder.name, + path: draftsFolder.path, + type: draftsFolder.type + }]); + return true; }), 'folders').yieldsAsync(); diff --git a/test/unit/update-handler-test.js b/test/unit/update-handler-test.js index ca07078..606be07 100644 --- a/test/unit/update-handler-test.js +++ b/test/unit/update-handler-test.js @@ -277,7 +277,7 @@ define(function(require) { var SMTP_DB_KEY = 'smtp'; var REALNAME_DB_KEY = 'realname'; var emailaddress = 'bla@blubb.io'; - + var imap = config.gmail.imap, smtp = config.gmail.smtp; @@ -347,6 +347,128 @@ define(function(require) { }); }); }); + + describe('v4 -> v5', function() { + var FOLDER_TYPE_INBOX = 'Inbox'; + var FOLDER_TYPE_SENT = 'Sent'; + var FOLDER_TYPE_DRAFTS = 'Drafts'; + var FOLDER_TYPE_TRASH = 'Trash'; + + var FOLDER_DB_TYPE = 'folders'; + var VERSION_DB_TYPE = 'dbVersion'; + + var POST_UPDATE_DB_VERSION = 5; + + beforeEach(function() { + cfg.dbVersion = 5; // app requires database version 4 + appConfigStorageStub.listItems.withArgs(VERSION_DB_TYPE).yieldsAsync(null, [4]); // database version is 4 + }); + + afterEach(function() { + // database version is only queried for version checking prior to the update script + // so no need to check this in case-specific tests + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + it('should work', function(done) { + userStorageStub.listItems.withArgs(FOLDER_DB_TYPE).yieldsAsync(null, [ + [{ + name: 'inbox1', + type: FOLDER_TYPE_INBOX + }, { + name: 'inbox2', + type: FOLDER_TYPE_INBOX + }, { + name: 'sent1', + type: FOLDER_TYPE_SENT + }, { + name: 'sent2', + type: FOLDER_TYPE_SENT + }, { + name: 'drafts1', + type: FOLDER_TYPE_DRAFTS + }, { + name: 'drafts2', + type: FOLDER_TYPE_DRAFTS + }, { + name: 'trash1', + type: FOLDER_TYPE_TRASH + }, { + name: 'trash2', + type: FOLDER_TYPE_TRASH + }] + ]); + + userStorageStub.storeList.withArgs([ + [{ + name: 'inbox1', + type: FOLDER_TYPE_INBOX + }, { + name: 'sent1', + type: FOLDER_TYPE_SENT + }, { + name: 'drafts1', + type: FOLDER_TYPE_DRAFTS + }, { + name: 'trash1', + type: FOLDER_TYPE_TRASH + }] + ], FOLDER_DB_TYPE).yieldsAsync(); + + appConfigStorageStub.storeList.withArgs([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting database version fails', function(done) { + userStorageStub.listItems.yieldsAsync(null, []); + userStorageStub.storeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting folders fails', function(done) { + userStorageStub.listItems.yieldsAsync(null, []); + userStorageStub.storeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + + it('should fail when listing folders fails', function(done) { + userStorageStub.listItems.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.called).to.be.false; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + }); }); }); }); \ No newline at end of file