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