From a7efdf112587098687d997a8bbda707d73c183f3 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 6 May 2014 17:23:08 +0200 Subject: [PATCH 1/8] [WO-286] adapt to changes in data model for use of signed msgs and html --- package.json | 4 +- src/js/app-config.js | 2 +- src/js/app-controller.js | 4 +- src/js/dao/email-dao.js | 211 ++++++++--------- src/js/dao/email-sync.js | 93 ++++---- src/js/util/update/update-handler.js | 5 +- src/js/util/update/update-v2.js | 28 +++ test/new-unit/email-dao-test.js | 337 +++++++++++++-------------- test/new-unit/email-sync-test.js | 179 +++++++------- test/new-unit/update-handler-test.js | 57 ++++- 10 files changed, 500 insertions(+), 420 deletions(-) create mode 100644 src/js/util/update/update-v2.js 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 From 747d9fce395ca73796f15f2922711d320b690db9 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 9 May 2014 16:01:44 +0200 Subject: [PATCH 2/8] Fix scroling in iframe in Chrome App --- src/js/controller/mail-list.js | 2 +- src/js/controller/read.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index b386e9e..08ce610 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -418,7 +418,7 @@ define(function(require) { '>> from 0.7.0.1\n' + '>>\n' + '>> God speed!'; // plaintext body - this.html = '

HTML content

'; + this.html = '


---------- Forwarded message ----------
From: MunichJS User Group <info@meetup.com>
Date: Thu, May 8, 2014 at 11:10 PM
Subject: Stay in touch!
To: mail@john.com


Meetup
Axel Rauschmayer
Axel Rauschmayer
Organizer
Good to see you
Béla Varga
Béla Varga
Co-Organizer
Good to see you
Alexander Schmidt
Alexander Schmidt
Good to see you
Amer Alimanovic
Amer Alimanovic
Good to see you
Ankit Bahuguna
Ankit Bahuguna
Good to see you
See all 91 people
Couldn't go? Click here.

Unsubscribe from similar emails from this Meetup Group

Add info@meetup.com to your address book to receive all Meetup emails

Meetup, POB 4668 #37895 NY NY USA 10163

Meetup HQ in NYC is hiring!meetup.com/jobs


'; this.encrypted = true; this.decrypted = true; }; diff --git a/src/js/controller/read.js b/src/js/controller/read.js index 6dc3258..da9e6dd 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -336,7 +336,10 @@ define(function(require) { scope.html = undefined; if (value) { $timeout(function() { - scope.html = $sce.trustAsHtml(value); + // wrap in html doc with scrollable html tag, since chrome apps does not scroll by default + var prefix = ''; + var suffix = ''; + scope.html = $sce.trustAsHtml(prefix + value + suffix); }); } }); @@ -353,4 +356,4 @@ define(function(require) { }); return ReadCtrl; -}); +}); \ No newline at end of file From 453422cf11c0c209011d7d0e94cd9fb214b06dad Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Fri, 9 May 2014 16:31:48 +0200 Subject: [PATCH 3/8] use new pgpbuilder --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f51e458..2f0dda2 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1", "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", + "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/dev/wo-286", + "pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/dev/wo-286", "requirejs": "2.1.10" }, "devDependencies": { From a97c3a35e7b821bbb1080c303b797d05950246bf Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 12 May 2014 13:18:51 +0200 Subject: [PATCH 4/8] [WO-381] Fix bug that displayed ciphertext instead of plaintext --- src/js/controller/read.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/controller/read.js b/src/js/controller/read.js index da9e6dd..2bf42bb 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -70,8 +70,10 @@ define(function(require) { $scope.node = undefined; }); $scope.$watch('state.mailList.selected.body', function(body) { - if (!body || (body && $scope.state.mailList.selected.decrypted === false)) { - $scope.node = undefined; + var selected = $scope.state.mailList.selected; + + $scope.node = undefined; // reset model + if (!body || (body && selected.encrypted && !selected.decrypted)) { return; } From 2a1a92f9073213dc03c72fbbcd806a98591ab7fa Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 12 May 2014 13:26:11 +0200 Subject: [PATCH 5/8] Dont show error popup on pgp decrypt error --- src/js/dao/email-dao.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 5bbff8a..4cf73ed 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -414,10 +414,8 @@ define(function(require) { 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); + message.body = err.errMsg || err.message; + done(); return; } From 99edcee93b9e3784279cb479a8153b8115ca7e82 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 12 May 2014 13:44:02 +0200 Subject: [PATCH 6/8] Show decrypting/parsing errors in mail reader --- src/js/controller/read.js | 9 ++++++--- src/js/dao/email-dao.js | 21 ++++++++++++--------- test/new-unit/email-dao-test.js | 5 +++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/js/controller/read.js b/src/js/controller/read.js index 2bf42bb..5f6f88e 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -70,10 +70,13 @@ define(function(require) { $scope.node = undefined; }); $scope.$watch('state.mailList.selected.body', function(body) { - var selected = $scope.state.mailList.selected; - $scope.node = undefined; // reset model - if (!body || (body && selected.encrypted && !selected.decrypted)) { + if (!body) { + return; + } + + var selected = $scope.state.mailList.selected; + if (selected.encrypted && !selected.decrypted) { return; } diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 4cf73ed..2ee8152 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -398,15 +398,13 @@ define(function(require) { // get the sender's public key for signature checking self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { if (err) { - message.decryptingBody = false; - callback(err); + done(err); return; } if (!senderPublicKey) { // this should only happen if a mail from another channel is in the inbox - message.body = 'Public key for sender not found!'; - done(); + showError('Public key for sender not found!'); return; } @@ -414,8 +412,7 @@ define(function(require) { var encryptedNode = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0]; self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) { if (err || !decrypted) { - message.body = err.errMsg || err.message; - done(); + showError(err.errMsg || err.message); return; } @@ -425,9 +422,9 @@ define(function(require) { // parse the decrpyted raw content in the mailparser self._mailreader.parse({ bodyParts: [encryptedNode] - }, function(error, parsedBodyParts) { - if (error) { - done(error); + }, function(err, parsedBodyParts) { + if (err) { + showError(err.errMsg || err.message); return; } @@ -450,6 +447,12 @@ define(function(require) { }); }); + function showError(msg) { + message.body = msg; + message.decrypted = true; // display error msh in body + done(); + } + function done(err) { message.decryptingBody = false; callback(err, err ? undefined : message); diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 7ac9d4d..6439815 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -994,8 +994,9 @@ define(function(require) { dao.decryptBody({ message: message }, function(error, msg) { - expect(error).to.exist; - expect(msg).to.not.exist; + expect(error).to.not.exist; + expect(msg.body).to.equal('asd'); + expect(msg).to.exist; expect(message.decryptingBody).to.be.false; expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; From c6a82b3442ca1800310165b99d5bba260f15b7c1 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Mon, 12 May 2014 13:57:25 +0200 Subject: [PATCH 7/8] [WO-290] Fix bug where plaintext mail body is not shown in outbox --- src/tpl/read.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tpl/read.html b/src/tpl/read.html index f3d7862..1a9f4e8 100644 --- a/src/tpl/read.html +++ b/src/tpl/read.html @@ -60,7 +60,7 @@
+ ng-if="!html && (state.mailList.selected === undefined || (!state.mailList.selected.encrypted && state.mailList.selected.body !== undefined) || (state.mailList.selected.encrypted === true && state.mailList.selected.decrypted === true))">
Date: Mon, 12 May 2014 14:53:51 +0200 Subject: [PATCH 8/8] Use tagged deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2f0dda2..e14de62 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,10 @@ }, "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1", - "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/dev/wo-286", - "pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/dev/wo-286", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.0", + "mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.3.0", + "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.0", + "pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.3.0", "requirejs": "2.1.10" }, "devDependencies": { @@ -36,4 +36,4 @@ "grunt-contrib-compress": "~0.5.2", "grunt-node-webkit-builder": "~0.1.17" } -} +} \ No newline at end of file