diff --git a/package.json b/package.json index 2026351..f51e458 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1", - "imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.2.6", - "mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.2.2", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-286", + "mailreader": "https://github.com/whiteout-io/mailreader/tarball/dev/wo-286", "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.2.2", "pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.2.3", "requirejs": "2.1.10" diff --git a/src/js/app-config.js b/src/js/app-config.js index 511fae7..cc0f818 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -46,7 +46,7 @@ define(function(require) { iconPath: '/img/icon.png', verificationUrl: '/verify/', verificationUuidLength: 36, - dbVersion: 1, + dbVersion: 2, appVersion: appVersion }; diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 844af7c..f00c318 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -72,7 +72,7 @@ define(function(require) { self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao); self._crypto = pgp = new PGP(); self._pgpbuilder = pgpbuilder = new PgpBuilder(); - self._emailSync = emailSync = new EmailSync(keychain, userStorage); + self._emailSync = emailSync = new EmailSync(keychain, userStorage, mailreader); self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); self._updateHandler = new UpdateHandler(appConfigStore, userStorage); @@ -129,7 +129,7 @@ define(function(require) { }; pgpMailer = new PgpMailer(smtpOptions, self._pgpbuilder); - imapClient = new ImapClient(imapOptions, mailreader); + imapClient = new ImapClient(imapOptions); imapClient.onError = onImapError; // connect to clients diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 981c26c..5bbff8a 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -274,123 +274,121 @@ define(function(require) { message = options.message, folder = options.folder; - if (message.loadingBody) { - return; - } - - // the message already has a body, so no need to become active here - if (message.body) { + // the message either already has a body or is fetching it right now, so no need to become active here + if (message.loadingBody || typeof message.body !== 'undefined') { return; } message.loadingBody = true; - // the mail does not have its content in memory - readFromDevice(); + /* + * read this before inspecting the method! + * + * you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior: + * 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array, + * because it is not really necessary to keep everything in memory + * 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property + * to the model. this property is auto generated and must not be persisted. + */ - // if possible, read the message body from the device - function readFromDevice() { + retrieveContent(); + + function retrieveContent() { + // load the local message from memory self._emailSync._localListMessages({ folder: folder, uid: message.uid }, function(err, localMessages) { - var localMessage; - - if (err) { - message.loadingBody = false; - callback(err); + if (err || localMessages.length === 0) { + done(err); return; } - localMessage = localMessages[0]; + var localMessage = localMessages[0]; - if (!localMessage.body) { - streamFromImap(); + // do we need to fetch content from the imap server? + var needsFetch = false; + localMessage.bodyParts.forEach(function(part) { + needsFetch = (typeof part.content === 'undefined'); + }); + + if (!needsFetch) { + // if we have all the content we need, + // we can extract the content + message.bodyParts = localMessage.bodyParts; + extractContent(); return; } - // attach the body to the mail object - message.body = localMessage.body; - handleEncryptedContent(); - }); - } - - // if reading the message body from the device was unsuccessful, - // stream the message from the imap server - function streamFromImap() { - self._emailSync._imapStreamText({ - folder: folder, - message: message - }, function(error) { - if (error) { - message.loadingBody = false; - callback(error); - return; - } - - message.loadingBody = false; - - // 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._emailSync._localListMessages({ + // get the raw content from the imap server + self._emailSync._getBodyParts({ folder: folder, - uid: message.uid - }, function(error, storedMessages) { - if (error) { - callback(error); + uid: localMessage.uid, + bodyParts: localMessage.bodyParts + }, function(err, parsedBodyParts) { + if (err) { + done(err); return; } - storedMessages[0].body = message.body; + message.bodyParts = parsedBodyParts; + localMessage.bodyParts = parsedBodyParts; + // persist it to disk self._emailSync._localStoreMessages({ folder: folder, - emails: storedMessages + emails: [localMessage] }, function(error) { if (error) { - callback(error); + done(error); return; } - handleEncryptedContent(); + // extract the content + extractContent(); }); }); }); } - function handleEncryptedContent() { - // normally, the imap-client should already have set the message.encrypted flag. problem: if we have pgp/inline, - // we can't reliably determine if the message is encrypted before we have inspected the payload... - message.encrypted = containsArmoredCiphertext(message); - - // cleans the message body from everything but the ciphertext + function extractContent() { if (message.encrypted) { - message.decrypted = false; - extractCiphertext(); + // show the encrypted message + message.body = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0].content; + done(); + return; } + + // for unencrypted messages, this is the array where the body parts are located + var root = message.bodyParts; + + if (message.signed) { + var signedPart = self._emailSync.filterBodyParts(message.bodyParts, 'signed')[0]; + message.message = signedPart.message; + message.signature = signedPart.signature; + // TODO check integrity + // in case of a signed message, you only want to show the signed content and ignore the rest + root = signedPart.content; + } + + message.attachments = self._emailSync.filterBodyParts(root, 'attachment'); + message.body = _.pluck(self._emailSync.filterBodyParts(root, 'text'), 'content').join('\n'); + message.html = _.pluck(self._emailSync.filterBodyParts(root, 'html'), 'content').join('\n'); + + done(); + } + + function done(err) { message.loadingBody = false; - callback(null, message); + callback(err, err ? undefined : message); } - - function containsArmoredCiphertext() { - return message.body.indexOf(str.cryptPrefix) !== -1 && message.body.indexOf(str.cryptSuffix) !== -1; - } - - function extractCiphertext() { - var start = message.body.indexOf(str.cryptPrefix), - end = message.body.indexOf(str.cryptSuffix) + str.cryptSuffix.length; - - // parse message body for encrypted message block - message.body = message.body.substring(start, end); - } - }; EmailDAO.prototype.decryptBody = function(options, callback) { var self = this, message = options.message; - // the message has no body, is not encrypted or has already been decrypted + // the message is decrypting has no body, is not encrypted or has already been decrypted if (message.decryptingBody || !message.body || !message.encrypted || message.decrypted) { return; } @@ -408,61 +406,56 @@ define(function(require) { if (!senderPublicKey) { // this should only happen if a mail from another channel is in the inbox message.body = 'Public key for sender not found!'; - message.decryptingBody = false; - callback(null, message); + done(); return; } // get the receiver's public key to check the message signature - self._crypto.decrypt(message.body, senderPublicKey.publicKey, function(err, decrypted) { - // if an error occurs during decryption, display the error message as the message content - decrypted = decrypted || err.errMsg || 'Error occurred during decryption'; - - // this is a very primitive detection if we have PGP/MIME or PGP/INLINE - if (!self._mailreader.isRfc(decrypted)) { - message.body = decrypted; - message.decrypted = true; - message.decryptingBody = false; - callback(null, message); + var encryptedNode = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0]; + self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) { + if (err || !decrypted) { + err = err || { + errMsg: 'Error occurred during decryption' + }; + done(err); return; } - // parse the decrypted MIME message - self._imapParseMessageBlock({ - message: message, - raw: decrypted - }, function(error) { + // the mailparser works on the .raw property + encryptedNode.raw = decrypted; + + // parse the decrpyted raw content in the mailparser + self._mailreader.parse({ + bodyParts: [encryptedNode] + }, function(error, parsedBodyParts) { if (error) { - message.decryptingBody = false; - callback(error); + done(error); return; } - message.decrypted = true; + // we have successfully interpreted the descrypted message, + // so let's update the views on the message parts - // remove the pgp-signature from the attachments - message.attachments = _.reject(message.attachments, function(attmt) { + message.body = _.pluck(self._emailSync.filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n'); + message.html = _.pluck(self._emailSync.filterBodyParts(parsedBodyParts, 'html'), 'content').join('\n'); + message.attachments = _.reject(self._emailSync.filterBodyParts(parsedBodyParts, 'attachment'), function(attmt) { + // remove the pgp-signature from the attachments return attmt.mimeType === "application/pgp-signature"; }); + message.decrypted = true; + + // we're done here! - message.decryptingBody = false; - callback(null, message); + done(); }); }); }); - }; - EmailDAO.prototype.getAttachment = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; + function done(err) { + message.decryptingBody = false; + callback(err, err ? undefined : message); } - - this._imapClient.getAttachment(options, callback); }; EmailDAO.prototype.sendEncrypted = function(options, callback) { @@ -541,10 +534,6 @@ define(function(require) { this._imapClient.logout(callback); }; - EmailDAO.prototype._imapParseMessageBlock = function(options, callback) { - this._mailreader.parseRfc(options, 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 index 374c730..f5eb2ca 100644 --- a/src/js/dao/email-sync.js +++ b/src/js/dao/email-sync.js @@ -5,9 +5,10 @@ define(function(require) { config = require('js/app-config').config, str = require('js/app-config').string; - var EmailSync = function(keychain, devicestorage) { + var EmailSync = function(keychain, devicestorage, mailreader) { this._keychain = keychain; this._devicestorage = devicestorage; + this._mailreader = mailreader; }; EmailSync.prototype.init = function(options, callback) { @@ -168,8 +169,8 @@ define(function(require) { } storedMessages.forEach(function(storedMessage) { - // remove the body to not load unnecessary data to memory - delete storedMessage.body; + // remove the body parts to not load unnecessary data to memory + delete storedMessage.bodyParts; folder.messages.push(storedMessage); }); @@ -619,10 +620,11 @@ define(function(require) { } function handleVerification(message, localCallback) { - self._imapStreamText({ + self._getBodyParts({ folder: options.folder, - message: message - }, function(error) { + uid: message.uid, + bodyParts: message.bodyParts + }, function(error, parsedBodyParts) { // 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) { @@ -630,8 +632,9 @@ define(function(require) { return; } - var verificationUrlPrefix = config.cloudUrl + config.verificationUrl, - uuid = message.body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength), + var body = _.pluck(self.filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n'), + verificationUrlPrefix = config.cloudUrl + config.verificationUrl, + uuid = 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 @@ -707,12 +710,8 @@ define(function(require) { return; } - this._imapClient.updateFlags({ - path: options.folder, - uid: options.uid, - unread: options.unread, - answered: options.answered - }, callback); + options.path = options.folder; + this._imapClient.updateFlags(options, callback); }; /** @@ -731,18 +730,8 @@ define(function(require) { 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); + options.path = options.folder; + this._imapClient.search(options, callback); }; EmailSync.prototype._imapDeleteMessage = function(options, callback) { @@ -754,10 +743,8 @@ define(function(require) { return; } - this._imapClient.deleteMessage({ - path: options.folder, - uid: options.uid - }, callback); + options.path = options.folder; + this._imapClient.deleteMessage(options, callback); }; /** @@ -778,20 +765,18 @@ define(function(require) { return; } - self._imapClient.listMessagesByUid({ - path: options.folder, - firstUid: options.firstUid, - lastUid: options.lastUid - }, callback); + options.path = options.folder; + self._imapClient.listMessages(options, callback); }; /** * Stream an email messsage's body * @param {String} options.folder The folder - * @param {Object} options.message The message, as retrieved by _imapListMessages + * @param {String} options.uid the message's uid + * @param {Object} options.bodyParts 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) { + EmailSync.prototype._getBodyParts = function(options, callback) { var self = this; if (!this._account.online) { @@ -802,11 +787,37 @@ define(function(require) { return; } - self._imapClient.getBody({ - path: options.folder, - message: options.message - }, callback); + options.path = options.folder; + self._imapClient.getBodyParts(options, function(err) { + if (err) { + callback(err); + return; + } + // interpret the raw content of the email + self._mailreader.parse(options, callback); + }); }; + /** + * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them + * @param {[type]} bodyParts The bodyParts array + * @param {[type]} type The type to look up + * @param {undefined} result Leave undefined, only used for recursion + */ + EmailSync.prototype.filterBodyParts = function(bodyParts, type, result) { + var self = this; + + result = result || []; + bodyParts.forEach(function(part) { + if (part.type === type) { + result.push(part); + } else if (Array.isArray(part.content)) { + self.filterBodyParts(part.content, type, result); + } + }); + return result; + }; + + return EmailSync; }); \ No newline at end of file diff --git a/src/js/util/update/update-handler.js b/src/js/util/update/update-handler.js index f8c6388..8836c93 100644 --- a/src/js/util/update/update-handler.js +++ b/src/js/util/update/update-handler.js @@ -2,7 +2,8 @@ define(function(require) { 'use strict'; var cfg = require('js/app-config').config, - updateV1 = require('js/util/update/update-v1'); + updateV1 = require('js/util/update/update-v1'), + updateV2 = require('js/util/update/update-v2'); /** * Handles database migration @@ -10,7 +11,7 @@ define(function(require) { var UpdateHandler = function(appConfigStorage, userStorage) { this._appConfigStorage = appConfigStorage; this._userStorage = userStorage; - this._updateScripts = [updateV1]; + this._updateScripts = [updateV1, updateV2]; }; /** diff --git a/src/js/util/update/update-v2.js b/src/js/util/update/update-v2.js new file mode 100644 index 0000000..03fdf50 --- /dev/null +++ b/src/js/util/update/update-v2.js @@ -0,0 +1,28 @@ +define(function() { + 'use strict'; + + /** + * Update handler for transition database version 1 -> 2 + * + * In database version 2, the stored email objects have to be purged, because the + * new data model stores information about the email structure in the property 'bodyParts'. + */ + function updateV2(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 2; + + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } + + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); + } + + return updateV2; +}); \ No newline at end of file diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index c9e5563..7ac9d4d 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -447,17 +447,6 @@ define(function(require) { }); }); - describe('_imapParseMessageBlock', function() { - it('should parse a message', function(done) { - var parseRfc = sinon.stub(mailreader, 'parseRfc').withArgs({}).yields(); - - dao._imapParseMessageBlock({}, function() { - expect(parseRfc.calledOnce).to.be.true; - done(); - }); - }); - }); - describe('_imapLogin', function() { it('should fail when disconnected', function(done) { dao.onDisconnect(null, function(err) { @@ -616,6 +605,10 @@ define(function(require) { describe('getBody', function() { + var folder = 'asdasdasdasdasd', + uid = 1234, + localListStub, localStoreStub, imapGetStub; + it('should not do anything if the message already has content', function() { var message = { body: 'bender is great!' @@ -629,11 +622,9 @@ define(function(require) { }); it('should read an unencrypted body from the device', function(done) { - var message, uid, folder, body, localListStub; + var message, body; - folder = 'asdasdasdasdasd'; body = 'bender is great! bender is great!'; - uid = 1234; message = { uid: uid }; @@ -642,10 +633,12 @@ define(function(require) { folder: folder, uid: uid }).yieldsAsync(null, [{ - body: body + bodyParts: [{ + type: 'text', + content: body + }] }]); - dao.getBody({ message: message, folder: folder @@ -653,8 +646,7 @@ define(function(require) { expect(err).to.not.exist; expect(msg).to.equal(message); - expect(msg.body).to.not.be.empty; - expect(msg.encrypted).to.be.false; + expect(msg.body).to.equal(body); expect(msg.loadingBody).to.be.false; expect(localListStub.calledOnce).to.be.true; @@ -665,20 +657,26 @@ define(function(require) { }); it('should read an encrypted body from the device', function(done) { - var message, uid, folder, body, localListStub; + var message, ct, pt; - folder = 'asdasdasdasdasd'; - body = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - uid = 1234; + pt = 'bender is great!'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; message = { - uid: uid + uid: uid, + encrypted: true }; localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [{ - body: body + bodyParts: [{ + type: 'text', + content: pt + }, { + type: 'encrypted', + content: ct + }] }]); dao.getBody({ @@ -688,9 +686,8 @@ define(function(require) { expect(err).to.not.exist; expect(msg).to.equal(message); - expect(msg.body).to.not.be.empty; + expect(msg.body).to.equal(ct); expect(msg.encrypted).to.be.true; - expect(msg.decrypted).to.be.false; expect(message.loadingBody).to.be.false; expect(localListStub.calledOnce).to.be.true; @@ -700,14 +697,17 @@ define(function(require) { expect(message.loadingBody).to.be.true; }); - it('should stream an unencrypted body from imap', function(done) { - var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; + it('should stream from imap and set plain text body', function(done) { + var message, body; folder = 'asdasdasdasdasd'; body = 'bender is great! bender is great!'; uid = 1234; message = { - uid: uid + uid: uid, + bodyParts: [{ + type: 'text' + }] }; localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ @@ -720,16 +720,14 @@ define(function(require) { emails: [message] }).yieldsAsync(); - imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { - expect(opts).to.deep.equal({ - folder: folder, - message: message - }); - - message.body = body; - cb(); - }); - + imapGetStub = sinon.stub(emailSync, '_getBodyParts').withArgs({ + folder: folder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: body + }]); dao.getBody({ message: message, @@ -738,12 +736,11 @@ define(function(require) { expect(err).to.not.exist; expect(msg).to.equal(message); - expect(msg.body).to.not.be.empty; - expect(msg.encrypted).to.be.false; + expect(msg.body).to.equal(body); expect(msg.loadingBody).to.be.false; - expect(localListStub.calledTwice).to.be.true; - expect(imapStreamStub.calledOnce).to.be.true; + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; done(); @@ -751,14 +748,19 @@ define(function(require) { expect(message.loadingBody).to.be.true; }); - it('should stream an encrypted body from imap', function(done) { - var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; + it('should stream from imap and set encrypted body', function(done) { + var message, ct, pt; - folder = 'asdasdasdasdasd'; - body = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - uid = 1234; + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; message = { - uid: uid + uid: uid, + encrypted: true, + bodyParts: [{ + type: 'text' + }, { + type: 'encrypted' + }] }; localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ @@ -771,15 +773,17 @@ define(function(require) { emails: [message] }).yieldsAsync(); - imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { - expect(opts).to.deep.equal({ - folder: folder, - message: message - }); - - message.body = body; - cb(); - }); + imapGetStub = sinon.stub(emailSync, '_getBodyParts').withArgs({ + folder: folder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: pt + }, { + type: 'encrypted', + content: ct + }]); dao.getBody({ @@ -789,13 +793,12 @@ define(function(require) { expect(err).to.not.exist; expect(msg).to.equal(message); - expect(msg.body).to.not.be.empty; + expect(msg.body).to.equal(ct); expect(msg.encrypted).to.be.true; - expect(msg.decrypted).to.be.false; expect(msg.loadingBody).to.be.false; - expect(localListStub.calledTwice).to.be.true; - expect(imapStreamStub.calledOnce).to.be.true; + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; done(); @@ -804,22 +807,19 @@ define(function(require) { }); it('fail to stream from imap due to error when persisting', function(done) { - var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; - - folder = 'asdasdasdasdasd'; - body = 'THIS IS THE BODY'; - uid = 1234; - message = { - uid: uid + var message = { + uid: uid, + bodyParts: [{ + type: 'text' + }] }; localListStub = sinon.stub(emailSync, '_localListMessages').yieldsAsync(null, [message]); localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yieldsAsync({}); - - imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { - message.body = body; - cb(); - }); + imapGetStub = sinon.stub(emailSync, '_getBodyParts').yieldsAsync(null, [{ + type: 'text', + content: 'bender is great! bender is great!' + }]); dao.getBody({ message: message, @@ -827,8 +827,8 @@ define(function(require) { }, function(err, msg) { expect(err).to.exist; expect(msg).to.not.exist; - expect(localListStub.calledTwice).to.be.true; - expect(imapStreamStub.calledOnce).to.be.true; + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; expect(message.loadingBody).to.be.false; @@ -838,21 +838,15 @@ define(function(require) { }); it('fail to stream from imap due to stream error', function(done) { - var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; - - folder = 'asdasdasdasdasd'; - uid = 1234; - message = { - uid: uid + var message = { + uid: uid, + bodyParts: [{ + type: 'text' + }] }; - localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [{}]); - - imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) { - message.body = body; - cb({}); - }); - + localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [message]); + imapGetStub = sinon.stub(emailSync, '_getBodyParts').yieldsAsync({}); localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); dao.getBody({ @@ -862,7 +856,7 @@ define(function(require) { expect(err).to.exist; expect(msg).to.not.exist; expect(localListStub.calledOnce).to.be.true; - expect(imapStreamStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; expect(localStoreStub.called).to.be.false; expect(message.loadingBody).to.be.false; @@ -873,55 +867,89 @@ define(function(require) { }); describe('decryptBody', function() { - it('should not do anything when the message is not encrypted', function() { + it('should do nothing when the message is not encrypted', function() { var message = { - encrypted: false + encrypted: false, + decrypted: true, + body: 'asd' }; dao.decryptBody({ message: message }); - - // should do nothing }); - it('should not do anything when the message is already decrypted', function() { + it('should do nothing when the message is already decrypted', function() { var message = { encrypted: true, - decrypted: true + decrypted: true, + body: 'asd' }; dao.decryptBody({ message: message }); + }); - // should do nothing + it('should do nothing when the message has no body', function() { + var message = { + encrypted: true, + decrypted: false, + body: '' + }; + + dao.decryptBody({ + message: message + }); + }); + + it('should do nothing when the message is decrypting', function() { + var message = { + encrypted: true, + decrypted: false, + body: 'asd', + decryptingBody: true + }; + + dao.decryptBody({ + message: message + }); }); it('decrypt a pgp/mime message', function(done) { - var message, parsedBody, mimeBody, parseStub; + var message, ct, pt, parsed, parseStub; + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + parsed = 'bender! bender! bender!'; message = { from: [{ address: 'asdasdasd' }], + body: ct, encrypted: true, - decrypted: false, - body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + bodyParts: [{ + type: 'encrypted', + content: ct + }] }; - mimeBody = 'Content-Type: asdasdasd'; - parsedBody = 'body? yes.'; keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, mimeBody); - parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb) { - expect(o.message).to.equal(message); - expect(o.raw).to.equal(mimeBody); - - o.message.body = parsedBody; - cb(null, o.message); - }); + pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt); + parseStub = sinon.stub(mailreader, 'parse').withArgs({ + bodyParts: [{ + type: 'encrypted', + content: ct, + raw: pt + }] + }).yieldsAsync(null, [{ + type: 'encrypted', + content: [{ + type: 'text', + content: parsed + }] + }]); dao.decryptBody({ message: message @@ -929,110 +957,71 @@ define(function(require) { expect(error).to.not.exist; expect(msg).to.equal(message); - expect(msg.decrypted).to.be.true; - expect(msg.body).to.equal(parsedBody); - expect(msg.decryptingBody).to.be.false; + expect(message.decrypted).to.be.true; + expect(message.body).to.equal(parsed); + expect(message.decryptingBody).to.be.false; expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.calledOnce).to.be.true; + mailreader.parse.restore(); done(); }); expect(message.decryptingBody).to.be.true; }); - it('decrypt a pgp/inline message', function(done) { - var message, plaintextBody, parseStub; - - message = { - from: [{ - address: 'asdasdasd' - }], - encrypted: true, - decrypted: false, - body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' - }; - - plaintextBody = 'body? yes.'; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, plaintextBody); - parseStub = sinon.stub(dao, '_imapParseMessageBlock'); - - dao.decryptBody({ - message: message - }, function(error, msg) { - expect(error).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.decrypted).to.be.true; - expect(msg.body).to.equal(plaintextBody); - expect(msg.decryptingBody).to.be.false; - - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(parseStub.called).to.be.false; - - done(); - }); - expect(message.decryptingBody).to.be.true; - }); - it('should fail during decryption message', function(done) { - var message, plaintextBody, parseStub, errMsg; - - message = { + var message = { from: [{ address: 'asdasdasd' }], + body: 'asdjafuad', encrypted: true, - decrypted: false, - body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + bodyParts: [{ + type: 'encrypted', + content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }] }; - plaintextBody = 'body? yes.'; - errMsg = 'yaddayadda'; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yields(null, mockKeyPair.publicKey); + var parseStub = sinon.spy(mailreader, 'parse'); + keychainStub.getReceiverPublicKey.yields(null, mockKeyPair.publicKey); pgpStub.decrypt.yields({ - errMsg: errMsg + errMsg: 'asd' }); - parseStub = sinon.stub(dao, '_imapParseMessageBlock'); dao.decryptBody({ message: message }, function(error, msg) { - expect(error).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.decrypted).to.be.true; - expect(msg.body).to.equal(errMsg); - expect(msg.decryptingBody).to.be.false; + expect(error).to.exist; + expect(msg).to.not.exist; + expect(message.decryptingBody).to.be.false; expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.called).to.be.false; + mailreader.parse.restore(); done(); }); }); it('should fail during key export', function(done) { - var message, parseStub; - - message = { + var message = { from: [{ address: 'asdasdasd' }], encrypted: true, - decrypted: false, - body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + body: 'asdjafuad', + bodyParts: [{ + type: 'encrypted', + content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }] }; + var parseStub = sinon.spy(mailreader, 'parse'); keychainStub.getReceiverPublicKey.yields({}); - parseStub = sinon.stub(dao, '_imapParseMessageBlock'); dao.decryptBody({ message: message @@ -1041,13 +1030,13 @@ define(function(require) { expect(msg).to.not.exist; - expect(message.decrypted).to.be.false; expect(message.decryptingBody).to.be.false; expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.called).to.be.false; expect(parseStub.called).to.be.false; + mailreader.parse.restore(); done(); }); }); diff --git a/test/new-unit/email-sync-test.js b/test/new-unit/email-sync-test.js index dc0f94d..434e1cb 100644 --- a/test/new-unit/email-sync-test.js +++ b/test/new-unit/email-sync-test.js @@ -3,6 +3,7 @@ define(function(require) { var EmailSync = require('js/dao/email-sync'), KeychainDAO = require('js/dao/keychain-dao'), + mailreader = require('mailreader'), ImapClient = require('imap-client'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), expect = chai.expect; @@ -13,8 +14,7 @@ define(function(require) { var emailSync, keychainStub, imapClientStub, devicestorageStub; var emailAddress, mockkeyId, dummyEncryptedMail, - dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid, - corruptedVerificationMail, corruptedVerificationUuid, + dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid, corruptedVerificationUuid, nonWhitelistedMail; beforeEach(function(done) { @@ -29,7 +29,9 @@ define(function(require) { address: 'qwe@qwe.de' }], subject: 'qweasd', - body: '-----BEGIN PGP MESSAGE-----\nasd\n-----END PGP MESSAGE-----', + bodyParts: [{ + type: 'encrypted' + }], unread: false, answered: false }; @@ -43,24 +45,13 @@ define(function(require) { 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 + bodyParts: [{ + type: 'text' + }], 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: [{ @@ -70,7 +61,9 @@ define(function(require) { 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', + bodyParts: [{ + type: 'text' + }], unread: false, answered: false, }; @@ -83,7 +76,9 @@ define(function(require) { address: 'qwe@qwe.de' }], subject: 'qweasd', - body: 'asd' + bodyParts: [{ + type: 'text' + }], }; mockKeyPair = { publicKey: { @@ -106,10 +101,11 @@ define(function(require) { imapClientStub = sinon.createStubInstance(ImapClient); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - emailSync = new EmailSync(keychainStub, devicestorageStub); + emailSync = new EmailSync(keychainStub, devicestorageStub, mailreader); expect(emailSync._keychain).to.equal(keychainStub); expect(emailSync._devicestorage).to.equal(devicestorageStub); + expect(emailSync._mailreader).to.equal(mailreader); // init emailSync.init({ @@ -154,6 +150,8 @@ define(function(require) { describe('_imapSearch', function() { + var path = 'FOLDAAAA'; + it('should fail when disconnected', function(done) { // this is set in the emailDao.onDisconnect emailSync._account.online = false; @@ -164,10 +162,9 @@ define(function(require) { }); }); - it('should work', function(done) { - var path = 'FOLDAAAA'; - + it('should list all uids', function(done) { imapClientStub.search.withArgs({ + folder: path, path: path }).yields(); @@ -175,10 +172,10 @@ define(function(require) { folder: path }, done); }); - it('should work', function(done) { - var path = 'FOLDAAAA'; + it('should list answered uids', function(done) { imapClientStub.search.withArgs({ + folder: path, path: path, answered: true }).yields(); @@ -188,10 +185,10 @@ define(function(require) { answered: true }, done); }); - it('should work', function(done) { - var path = 'FOLDAAAA'; + it('should list unread uids', function(done) { imapClientStub.search.withArgs({ + folder: path, path: path, unread: true }).yields(); @@ -204,6 +201,9 @@ define(function(require) { }); describe('_imapDeleteMessage', function() { + var path = 'FOLDAAAA', + uid = 1337; + it('should fail when disconnected', function(done) { // this is set in the emailDao.onDisconnect emailSync._account.online = false; @@ -215,11 +215,9 @@ define(function(require) { }); it('should work', function(done) { - var path = 'FOLDAAAA', - uid = 1337; - imapClientStub.deleteMessage.withArgs({ path: path, + folder: path, uid: uid }).yields(); @@ -231,13 +229,15 @@ define(function(require) { }); describe('_imapListMessages', function() { - it('should work', function(done) { - var path = 'FOLDAAAA', - firstUid = 1337, - lastUid = 1339; + var path = 'FOLDAAAA', + firstUid = 1337, + lastUid = 1339; - imapClientStub.listMessagesByUid.withArgs({ + + it('should work', function(done) { + imapClientStub.listMessages.withArgs({ path: path, + folder: path, firstUid: firstUid, lastUid: lastUid }).yields(null, []); @@ -250,18 +250,14 @@ define(function(require) { expect(err).to.not.exist; expect(msgs).to.exist; - expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; + expect(imapClientStub.listMessages.calledOnce).to.be.true; done(); }); }); - it('should not work when listMessagesByUid fails', function(done) { - var path = 'FOLDAAAA', - firstUid = 1337, - lastUid = 1339; - - imapClientStub.listMessagesByUid.yields({}); + it('should not work when listMessages fails', function(done) { + imapClientStub.listMessages.yields({}); emailSync._imapListMessages({ folder: path, @@ -271,7 +267,7 @@ define(function(require) { expect(err).to.exist; expect(msgs).to.not.exist; - expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; + expect(imapClientStub.listMessages.calledOnce).to.be.true; done(); }); @@ -288,42 +284,50 @@ define(function(require) { }); }); - describe('_imapStreamText', function() { + describe('_getBodyParts', function() { + var path = 'FOLDAAAA', + parseStub; + it('should work', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.getBody.withArgs({ - path: path, - message: {} - }).yields(null, {}); - - emailSync._imapStreamText({ + var o = { folder: path, - message: {} - }, function(err, msg) { + uid: 123, + bodyParts: [] + }; + + imapClientStub.getBodyParts.withArgs(o).yields(null, {}); + parseStub = sinon.stub(mailreader, 'parse').withArgs(o).yields(null, []); + + emailSync._getBodyParts(o, function(err, parts) { expect(err).to.not.exist; - expect(msg).to.exist; + expect(parts).to.exist; - expect(imapClientStub.getBody.calledOnce).to.be.true; + expect(imapClientStub.getBodyParts.calledOnce).to.be.true; + expect(parseStub.calledOnce).to.be.true; + mailreader.parse.restore(); done(); }); }); it('should not work when getBody fails', function(done) { - var path = 'FOLDAAAA'; - - imapClientStub.getBody.yields({}); - - emailSync._imapStreamText({ + var o = { folder: path, - message: {} - }, function(err, msg) { + uid: 123, + bodyParts: [] + }; + + imapClientStub.getBodyParts.yields({}); + parseStub = sinon.spy(mailreader, 'parse'); + + emailSync._getBodyParts(o, function(err, msg) { expect(err).to.exist; expect(msg).to.not.exist; - expect(imapClientStub.getBody.calledOnce).to.be.true; + expect(imapClientStub.getBodyParts.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; + mailreader.parse.restore(); done(); }); }); @@ -332,7 +336,7 @@ define(function(require) { // this is set in the emailDao.onDisconnect emailSync._account.online = false; - emailSync._imapStreamText({}, function(err) { + emailSync._getBodyParts({}, function(err) { expect(err.code).to.equal(42); done(); }); @@ -950,13 +954,12 @@ define(function(require) { }).yields(null, []); imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); - - imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); - + imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{ + type: 'text', + content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, + }]); keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); - localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); - imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').withArgs({ folder: folder, uid: verificationMail.uid @@ -1012,7 +1015,10 @@ define(function(require) { answered: true }).yields(null, []); imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); - imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{ + type: 'text', + content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, + }]); keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({}); @@ -1073,7 +1079,10 @@ define(function(require) { answered: true }).yields(null, []); imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); - imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{ + type: 'text', + content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, + }]); keychainStub.verifyPublicKey.withArgs(verificationUuid).yields({ errMsg: 'fubar' }); @@ -1134,7 +1143,7 @@ define(function(require) { imapSearchStub = sinon.stub(emailSync, '_imapSearch'); imapSearchStub.withArgs({ folder: folder - }).yields(null, [corruptedVerificationMail.uid]); + }).yields(null, [verificationMail.uid]); imapSearchStub.withArgs({ folder: folder, unread: true @@ -1146,12 +1155,15 @@ define(function(require) { localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ folder: folder, - emails: [corruptedVerificationMail] + emails: [verificationMail] }).yields(); - imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [corruptedVerificationMail]); - imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null); + imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{ + type: 'text', + content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + corruptedVerificationUuid, + }]); keychainStub.verifyPublicKey.withArgs(corruptedVerificationUuid).yields({ errMsg: 'fubar' }); @@ -1602,19 +1614,16 @@ define(function(require) { describe('mark', function() { it('should work', function(done) { - imapClientStub.updateFlags.withArgs({ - path: 'asdf', - uid: 1, - unread: false, - answered: false - }).yields(); - - emailSync._imapMark({ + var o = { folder: 'asdf', uid: 1, unread: false, answered: false - }, function(err) { + }; + + imapClientStub.updateFlags.withArgs(o).yields(); + + emailSync._imapMark(o, function(err) { expect(imapClientStub.updateFlags.calledOnce).to.be.true; expect(err).to.not.exist; done(); diff --git a/test/new-unit/update-handler-test.js b/test/new-unit/update-handler-test.js index fd96ae6..6bec837 100644 --- a/test/new-unit/update-handler-test.js +++ b/test/new-unit/update-handler-test.js @@ -6,11 +6,11 @@ define(function(require) { UpdateHandler = require('js/util/update/update-handler'), expect = chai.expect; - chai.Assertion.includeStack = true; - describe('UpdateHandler', function() { var updateHandler, appConfigStorageStub, userStorageStub, origDbVersion; + chai.Assertion.includeStack = true; + beforeEach(function() { origDbVersion = cfg.dbVersion; appConfigStorageStub = sinon.createStubInstance(DeviceStorageDAO); @@ -160,6 +160,59 @@ define(function(require) { }); }); }); + + describe('v1 -> v2', function() { + var emailDbType = 'email_'; + + beforeEach(function() { + cfg.dbVersion = 2; // app requires database version 2 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [1]); // database version is 0 + }); + + 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.removeList.withArgs(emailDbType).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([2], versionDbType).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting database version fails', function(done) { + userStorageStub.removeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync({}); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when wiping emails from database fails', function(done) { + userStorageStub.removeList.yieldsAsync({}); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + }); }); }); }); \ No newline at end of file