From 0e9f68abeef6db9214e946b1537c052179658653 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Fri, 14 Feb 2014 17:29:16 +0100 Subject: [PATCH] change api of emaildao to load bodies on demand --- package.json | 2 +- src/js/dao/email-dao.js | 567 ++++++++------- test/new-unit/email-dao-test.js | 1167 ++++++++++++++++--------------- 3 files changed, 918 insertions(+), 818 deletions(-) diff --git a/package.json b/package.json index 8f17a48..80488d8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master", - "imap-client": "https://github.com/whiteout-io/imap-client/tarball/master", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/stream-plaintext", "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master", "requirejs": "2.1.10" }, diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 7780702..5eec539 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -207,10 +207,7 @@ define(function(require) { */ var self = this, - folder, - delta1 /*, delta2 */ , delta3, delta4, //message - deltaF2, deltaF4, - isFolderInitialized; + folder, isFolderInitialized; // validate options @@ -258,30 +255,13 @@ define(function(require) { return; } - if (_.isEmpty(storedMessages)) { - // if there's nothing here, we're good - callback(); - doImapDelta(); - return; - } - - var after = _.after(storedMessages.length, function() { - callback(); - doImapDelta(); - }); - storedMessages.forEach(function(storedMessage) { - handleMessage(storedMessage, function(err, cleartextMessage) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - folder.messages.push(cleartextMessage); - after(); - }); + delete storedMessage.body; // do not flood the memory + folder.messages.push(storedMessage); }); + + callback(); + doImapDelta(); }); } @@ -295,18 +275,16 @@ define(function(require) { return; } - /* - * delta1: storage > memory => we deleted messages, remove from remote - * delta2: memory > storage => we added messages, push to remote - * deltaF2: memory > storage => we changed flags, sync them to the remote and memory - */ - delta1 = checkDelta(storedMessages, folder.messages); - // delta2 = checkDelta(folder.messages, storedMessages); // not supported yet - deltaF2 = checkFlags(folder.messages, storedMessages); - doDelta1(); + /* + * delta1: storage > memory => we deleted messages, remove from remote + */ function doDelta1() { + var inMemoryUids = _.pluck(folder.messages, 'uid'), + storedMessageUids = _.pluck(storedMessages, 'uid'), + delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids + if (_.isEmpty(delta1)) { doDeltaF2(); return; @@ -317,10 +295,10 @@ define(function(require) { }); // deltaF2 contains references to the in-memory messages - delta1.forEach(function(inMemoryMessage) { + delta1.forEach(function(inMemoryUid) { var deleteMe = { folder: folder.path, - uid: inMemoryMessage.uid + uid: inMemoryUid }; self._imapDeleteMessage(deleteMe, function(err) { @@ -343,7 +321,12 @@ define(function(require) { }); } + /* + * deltaF2: memory > storage => we changed flags, sync them to the remote and memory + */ function doDeltaF2() { + var deltaF2 = checkFlags(folder.messages, storedMessages); // deltaf2 contains the message objects, we need those to sync the flags + if (_.isEmpty(deltaF2)) { callback(); doImapDelta(); @@ -397,48 +380,37 @@ define(function(require) { function doImapDelta() { self._imapSearch({ folder: folder.path - }, function(err, uids) { + }, function(err, inImapUids) { if (err) { self._account.busy = false; callback(err); return; } - // uidWrappers is just to wrap the bare uids in an object { uid: 123 } so - // the checkDelta function can treat it like something that resembles a stripped down email object... - var uidWrappers = _.map(uids, function(uid) { - return { - uid: uid - }; - }); + doDelta3(); /* * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage - * delta4: imap > memory => we have new messages available, fetch to memory and storage */ - delta3 = checkDelta(folder.messages, uidWrappers); - delta4 = checkDelta(uidWrappers, folder.messages); - - doDelta3(); - - // we deleted messages directly from the remote, remove from memory and storage function doDelta3() { + var inMemoryUids = _.pluck(folder.messages, 'uid'), + delta3 = _.difference(inMemoryUids, inImapUids); + if (_.isEmpty(delta3)) { doDelta4(); return; } var after = _.after(delta3.length, function() { - // we're done with delta 3, so let's continue doDelta4(); }); // delta3 contains references to the in-memory messages that have been deleted from the remote - delta3.forEach(function(inMemoryMessage) { + delta3.forEach(function(inMemoryUid) { // remove delta3 from local storage self._localDeleteMessage({ folder: folder.path, - uid: inMemoryMessage.uid + uid: inMemoryUid }, function(err) { if (err) { self._account.busy = false; @@ -446,85 +418,98 @@ define(function(require) { return; } - // remove delta3 from memory - var idx = folder.messages.indexOf(inMemoryMessage); - folder.messages.splice(idx, 1); + // remove the uid from memory + var inMemoryMessage = _.findWhere(folder.messages, function(msg) { + return msg.uid.a === inMemoryUid; + }); + folder.messages.splice(folder.messages.indexOf(inMemoryMessage), 1); after(); }); }); } - // we have new messages available, fetch to memory and storage - // (downstream sync) + + /* + * delta4: imap > memory => we have new messages available, fetch downstream to memory and storage + */ function doDelta4() { + var inMemoryUids = _.pluck(folder.messages, 'uid'), + delta4 = _.difference(inImapUids, inMemoryUids); + + // no delta, we're done here + if (_.isEmpty(delta4)) { + doDeltaF4(); + return; + } + // eliminate uids smaller than the biggest local uid, i.e. just fetch everything // that came in AFTER the most recent email we have in memory. Keep in mind that // uids are strictly ascending, so there can't be a NEW mail in the mailbox with a // uid smaller than anything we've encountered before. - if (!_.isEmpty(folder.messages)) { - var localUids = _.pluck(folder.messages, 'uid'), - maxLocalUid = Math.max.apply(null, localUids); + if (!_.isEmpty(inMemoryUids)) { + var maxInMemoryUid = Math.max.apply(null, inMemoryUids); // apply works with separate arguments rather than an array - // eliminate everything prior to maxLocalUid - delta4 = _.filter(delta4, function(uidWrapper) { - return uidWrapper.uid > maxLocalUid; + // eliminate everything prior to maxInMemoryUid, that was already synced + delta4 = _.filter(delta4, function(uid) { + return uid > maxInMemoryUid; }); } - // sync in the uids in ascending order, to not leave the local database in a corrupted state: - // when the 5, 3, 1 should be synced and the client would fail at 3, but 5 was successfully synced, - // any subsequent syncs would never fetch 1 and 3. simple solution: sync in ascending order - delta4 = _.sortBy(delta4, function(uidWrapper) { - return uidWrapper.uid; - }); - - syncNextItem(); - - function syncNextItem() { - // no delta, we're done here - if (_.isEmpty(delta4)) { - doDeltaF4(); + self._imapListMessages({ + folder: folder.path, + firstUid: Math.min.apply(null, delta4), + lastUid: Math.max.apply(null, delta4) + }, function(err, messages) { + if (err) { + self._account.busy = false; + callback(err); return; } - // delta4 contains the headers that are newly available on the remote - var nextUidWrapper = delta4.shift(); + // if there is a verification message in the synced messages, handle it + var verificationMessage = _.findWhere(messages, { + subject: str.subjectPrefix + str.verificationSubject + }); - // get the whole message - self._imapGetMessage({ - folder: folder.path, - uid: nextUidWrapper.uid - }, function(err, message) { - if (err) { - self._account.busy = false; - callback(err); + if (verificationMessage) { + handleVerification(verificationMessage, function(err) { + // TODO: show usable error when the verification failed + if (err) { + self._account.busy = false; + callback(err); + return; + } + + storeHeaders(); + }); + return; + } + + storeHeaders(); + + function storeHeaders() { + // eliminate non-whiteout mails + messages = _.filter(messages, function(message) { + // we don't want to display "[whiteout] "-prefixed mails for now + return message.subject.indexOf(str.subjectPrefix) === 0 && message.subject !== (str.subjectPrefix + str.verificationSubject); + }); + + // no delta, we're done here + if (_.isEmpty(messages)) { + doDeltaF4(); return; } - // imap filtering is insufficient, since google ignores non-alphabetical characters - if (message.subject.indexOf(str.subjectPrefix) === -1) { - syncNextItem(); - return; - } + // filter out the "[whiteout] " prefix + messages.forEach(function(messages) { + messages.subject = messages.subject.replace(/^\[whiteout\] /, ''); + }); - if (isVerificationMail(message)) { - verify(message, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - syncNextItem(); - }); - return; - } - - // add the encrypted message to the local storage + // persist the encrypted message to the local storage self._localStoreMessages({ folder: folder.path, - emails: [message] + emails: messages }, function(err) { if (err) { self._account.busy = false; @@ -532,25 +517,21 @@ define(function(require) { return; } - // decrypt and add to folder in memory - handleMessage(message, function(err, cleartextMessage) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - folder.messages.push(cleartextMessage); - syncNextItem(); - }); + // if persisting worked, add them to the messages array + folder.messages = folder.messages.concat(messages); + doDeltaF4(); }); - }); - } + } + }); } }); + /** + * deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory + */ function doDeltaF4() { - var answeredUids, unreadUids; + var answeredUids, unreadUids, + deltaF4 = []; getUnreadUids(); @@ -593,9 +574,6 @@ define(function(require) { } function updateFlags() { - // deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory - deltaF4 = []; - folder.messages.forEach(function(msg) { // if the message's uid is among the uids that should be unread, // AND the message is not unread, we clearly have to change that @@ -686,27 +664,6 @@ define(function(require) { } } - /* - * Checks which messages are included in a, but not in b - */ - function checkDelta(a, b) { - var i, msg, exists, - delta = []; - - // find the delta - for (i = a.length - 1; i >= 0; i--) { - msg = a[i]; - exists = _.findWhere(b, { - uid: msg.uid - }); - if (!exists) { - delta.push(msg); - } - } - - return delta; - } - /* * checks if there are some flags that have changed in a and b */ @@ -728,144 +685,210 @@ define(function(require) { return delta; } - function isVerificationMail(email) { - return email.subject === str.subjectPrefix + str.verificationSubject; - } + function handleVerification(message, localCallback) { + self._imapStreamText({ + folder: options.folder, + message: message + }, function(error) { + var verificationUrlPrefix = config.cloudUrl + config.verificationUrl, + uuid, isValidUuid, index; - function verify(email, localCallback) { - var uuid, isValidUuid, index, verifyUrlPrefix = config.cloudUrl + config.verificationUrl; - - if (!email.unread) { - // don't bother if the email was already marked as read - localCallback(); - return; - } - - index = email.body.indexOf(verifyUrlPrefix); - if (index === -1) { - // there's no url in the email, so forget about that. - localCallback(); - return; - } - - uuid = email.body.substr(index + verifyUrlPrefix.length, 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); - if (!isValidUuid) { - // there's no valid uuid in the email, so forget about that, too. - localCallback(); - return; - } - - self._keychain.verifyPublicKey(uuid, function(err) { - if (err) { - localCallback({ - errMsg: 'Verifying your public key failed: ' + err.errMsg - }); + if (error) { + localCallback(error); return; } - // public key has been verified, mark the message as read, delete it, and ignore it in the future - self._imapMark({ - folder: options.folder, - uid: email.uid, - unread: false - }, function(err) { + index = message.body.indexOf(verificationUrlPrefix); + if (index === -1) { + // there's no url in the message, so forget about that. + localCallback(); + return; + } + + uuid = message.body.substr(index + verificationUrlPrefix.length, 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); + if (!isValidUuid) { + // there's no valid uuid in the message, so forget about that, too. + localCallback(); + return; + } + + self._keychain.verifyPublicKey(uuid, function(err) { if (err) { - localCallback(err); + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }); return; } + // public key has been verified, delete the message self._imapDeleteMessage({ folder: options.folder, - uid: email.uid + uid: message.uid }, localCallback); }); }); } + }; - function handleMessage(message, localCallback) { - message.subject = message.subject.split(str.subjectPrefix)[1]; + /** + * Streams message content + * @param {Object} options.message The message for which to retrieve the body + * @param {Object} options.folder The IMAP folder + * @param {Function} callback(error, message) Invoked when the message is streamed, or provides information if an error occurred + */ + EmailDAO.prototype.getMessageContent = function(options, callback) { + var self = this, + message = options.message, + folder = options.folder; - if (containsArmoredCiphertext(message)) { - decrypt(message, localCallback); - return; - } - - // cleartext mail - localCallback(null, message); + // the message already has a body, so no need to become active here + if (message.body) { + callback(null, message); + return; } - function containsArmoredCiphertext(email) { - return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1; - } + // the mail does not have its content in memory + readFromDevice(); - function decrypt(email, localCallback) { - var sender; + // if possible, read the message body from the device + function readFromDevice() { + self._localListMessages({ + folder: folder, + uid: message.uid + }, function(err, localMessages) { + var localMessage; - extractArmoredContent(email); - - // fetch public key required to verify signatures - sender = email.from[0].address; - self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) { if (err) { - localCallback(err); + callback(err); return; } - if (!senderPubkey) { - // this should only happen if a mail from another channel is in the inbox - email.body = 'Public key for sender not found!'; - localCallback(null, email); + localMessage = localMessages[0]; + + if (!localMessage.body) { + streamFromImap(); return; } - // decrypt and verfiy signatures - self._crypto.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) { - if (err) { - decrypted = err.errMsg; - } + // attach the body to the mail object + message.body = localMessage.body; + handleEncryptedContent(); + }); + } - // set encrypted flag - email.encrypted = true; + // if reading the message body from the device was unsuccessful, + // stream the message from the imap server + function streamFromImap() { + self._imapStreamText({ + folder: folder, + message: message + }, function(error) { + if (error) { + callback(error); + return; + } - // does our message block even need to be parsed? - // this is a very primitive detection if we have a mime node or plain text - // taking this out breaks compatibility to clients < 0.5 - if (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && - decrypted.indexOf('Content-Type:') === -1) { - // decrypted message is plain text and not a well-formed email - email.body = decrypted; - localCallback(null, email); + self._localStoreMessages({ + folder: folder, + emails: [message] + }, function(error) { + if (error) { + callback(error); return; } - // parse decrypted message - self._imapParseMessageBlock({ - message: email, - block: decrypted - }, function(error, parsedMessage) { - if (!parsedMessage) { - localCallback(error); - return; - } - - // remove the pgp-signature from the attachments - parsedMessage.attachments = _.reject(parsedMessage.attachments, function(attmt) { - return attmt.mimeType === "application/pgp-signature"; - }); - localCallback(error, parsedMessage); - }); + handleEncryptedContent(); }); }); - - function extractArmoredContent(email) { - var start = email.body.indexOf(str.cryptPrefix), - end = email.body.indexOf(str.cryptSuffix) + str.cryptSuffix.length; - - // parse email body for encrypted message block - email.body = email.body.substring(start, end); - } } + + 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 + if (message.encrypted) { + message.decrypted = false; + extractCiphertext(); + } + callback(null, 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.decryptMessageContent = function(options, callback) { + var self = this, + message = options.message; + + // the message is not encrypted or has already been decrypted + if (!message.encrypted || message.decrypted) { + callback(null, message); + return; + } + + // get the sender's public key for signature checking + self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + if (err) { + callback(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!'; + callback(null, message); + 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 (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && decrypted.indexOf('Content-Type:') === -1) { + message.body = decrypted; + message.decrypted = true; + callback(null, message); + return; + } + + // parse the decrypted MIME message + self._imapParseMessageBlock({ + message: message, + block: decrypted + }, function(error) { + if (error) { + callback(error); + return; + } + + message.decrypted = true; + + // remove the pgp-signature from the attachments + message.attachments = _.reject(message.attachments, function(attmt) { + return attmt.mimeType === "application/pgp-signature"; + }); + + // we're done here! + callback(error, message); + }); + }); + }); }; EmailDAO.prototype._imapMark = function(options, callback) { @@ -1099,10 +1122,13 @@ define(function(require) { }; /** - * Get an email messsage including the email body from imap - * @param {String} options.messageId The + * Get an email messsage without the body + * @param {String} options.folder The folder + * @param {Number} options.firstUid The lower bound of the uid (inclusive) + * @param {Number} options.lastUid The upper bound of the uid range (inclusive) + * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata */ - EmailDAO.prototype._imapGetMessage = function(options, callback) { + EmailDAO.prototype._imapListMessages = function(options, callback) { var self = this; if (!this._account.online) { @@ -1113,17 +1139,34 @@ define(function(require) { return; } - self._imapClient.getMessage({ + self._imapClient.listMessagesByUid({ path: options.folder, - uid: options.uid - }, function(err, message) { - if (err) { - callback(err); - return; - } + firstUid: options.firstUid, + lastUid: options.lastUid + }, callback); + }; - callback(null, message); - }); + /** + * Stream an email messsage's body + * @param {String} options.folder The folder + * @param {Object} options.message The message, as retrieved by _imapListMessages + * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content + */ + EmailDAO.prototype._imapStreamText = function(options, callback) { + var self = this; + + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + self._imapClient.streamPlaintext({ + path: options.folder, + message: options.message + }, callback); }; /** diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 2d696e6..7cf6ff5 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -10,6 +10,7 @@ define(function(require) { str = require('js/app-config').string, expect = chai.expect; + chai.Assertion.includeStack = true; describe('Email DAO unit tests', function() { var dao, keychainStub, imapClientStub, pgpMailerStub, pgpStub, devicestorageStub; @@ -757,57 +758,115 @@ define(function(require) { }); }); - describe('_imapGetMessage', function() { + describe('_imapListMessages', function() { + it('should work', function(done) { + var path = 'FOLDAAAA', + firstUid = 1337, + lastUid = 1339; + + imapClientStub.listMessagesByUid.withArgs({ + path: path, + firstUid: firstUid, + lastUid: lastUid + }).yields(null, []); + + dao._imapListMessages({ + folder: path, + firstUid: firstUid, + lastUid: lastUid + }, function(err, msgs) { + expect(err).to.not.exist; + expect(msgs).to.exist; + + expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not work when listMessagesByUid fails', function(done) { + var path = 'FOLDAAAA', + firstUid = 1337, + lastUid = 1339; + + imapClientStub.listMessagesByUid.yields({}); + + dao._imapListMessages({ + folder: path, + firstUid: firstUid, + lastUid: lastUid + }, function(err, msgs) { + expect(err).to.exist; + expect(msgs).to.not.exist; + + expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true; + + done(); + }); + }); + it('should fail when disconnected', function(done) { dao.onDisconnect(null, function(err) { expect(err).to.not.exist; - dao._imapGetMessage({}, function(err) { + dao._imapListMessages({}, function(err) { expect(err.code).to.equal(42); done(); }); }); }); + }); + describe('_imapStreamText', function() { it('should work', function(done) { - var path = 'FOLDAAAA', - uid = 1337; + var path = 'FOLDAAAA'; - imapClientStub.getMessage.withArgs({ + imapClientStub.streamPlaintext.withArgs({ path: path, - uid: uid + message: {} }).yields(null, {}); - dao._imapGetMessage({ + dao._imapStreamText({ folder: path, - uid: uid + message: {} }, function(err, msg) { expect(err).to.not.exist; expect(msg).to.exist; - expect(imapClientStub.getMessage.calledOnce).to.be.true; + expect(imapClientStub.streamPlaintext.calledOnce).to.be.true; done(); }); }); - it('should not work when getMessage fails', function(done) { - var path = 'FOLDAAAA', - uid = 1337; - imapClientStub.getMessage.yields({}); + it('should not work when streamPlaintext fails', function(done) { + var path = 'FOLDAAAA'; - dao._imapGetMessage({ + imapClientStub.streamPlaintext.yields({}); + + dao._imapStreamText({ folder: path, - uid: uid + message: {} }, function(err, msg) { expect(err).to.exist; expect(msg).to.not.exist; - expect(imapClientStub.getMessage.calledOnce).to.be.true; + expect(imapClientStub.streamPlaintext.calledOnce).to.be.true; done(); }); }); + + it('should fail when disconnected', function(done) { + dao.onDisconnect(null, function(err) { + expect(err).to.not.exist; + + dao._imapStreamText({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + }); }); describe('_localListMessages', function() { @@ -869,9 +928,424 @@ define(function(require) { }); }); + describe('getMessageContent', function() { + it('should not do anything if the message already has content', function(done) { + var message = { + body: 'bender is great!' + }; + + dao.getMessageContent({ + message: message + }, function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.equal(message); + + done(); + }); + }); + + it('should read an unencrypted body from the device', function(done) { + var message, uid, folder, body, localListStub; + + folder = 'asdasdasdasdasd'; + body = 'bender is great! bender is great!'; + uid = 1234; + message = { + uid: uid + }; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder, + uid: uid + }).yields(null, [{ + body: body + }]); + + + dao.getMessageContent({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.equal(message); + expect(msg.body).to.not.be.empty; + expect(msg.encrypted).to.be.false; + expect(localListStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should read an encrypted body from the device', function(done) { + var message, uid, folder, body, localListStub; + + folder = 'asdasdasdasdasd'; + body = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + uid = 1234; + message = { + uid: uid + }; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder, + uid: uid + }).yields(null, [{ + body: body + }]); + + + dao.getMessageContent({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.equal(message); + expect(msg.body).to.not.be.empty; + expect(msg.encrypted).to.be.true; + expect(msg.decrypted).to.be.false; + expect(localListStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should stream an unencrypted body from imap', function(done) { + var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; + + folder = 'asdasdasdasdasd'; + body = 'bender is great! bender is great!'; + uid = 1234; + message = { + uid: uid + }; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder, + uid: uid + }).yields(null, [{}]); + + localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ + folder: folder, + emails: [message] + }).yields(); + + imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + expect(opts).to.deep.equal({ + folder: folder, + message: message + }); + + message.body = body; + cb(); + }); + + + dao.getMessageContent({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.equal(message); + expect(msg.body).to.not.be.empty; + expect(msg.encrypted).to.be.false; + expect(localListStub.calledOnce).to.be.true; + expect(imapStreamStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should stream an encrypted body from imap', function(done) { + var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; + + folder = 'asdasdasdasdasd'; + body = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + uid = 1234; + message = { + uid: uid + }; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder, + uid: uid + }).yields(null, [{}]); + + localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ + folder: folder, + emails: [message] + }).yields(); + + imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + expect(opts).to.deep.equal({ + folder: folder, + message: message + }); + + message.body = body; + cb(); + }); + + + dao.getMessageContent({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.equal(message); + expect(msg.body).to.not.be.empty; + expect(msg.encrypted).to.be.true; + expect(msg.decrypted).to.be.false; + expect(localListStub.calledOnce).to.be.true; + expect(imapStreamStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + + done(); + }); + }); + + it('fail to stream from imap due to error when persisting', function(done) { + var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub; + + folder = 'asdasdasdasdasd'; + uid = 1234; + message = { + uid: uid + }; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder, + uid: uid + }).yields(null, [{}]); + + imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + message.body = body; + cb(); + }); + + localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ + folder: folder, + emails: [message] + }).yields({}); + + dao.getMessageContent({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.exist; + expect(msg).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(imapStreamStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + }); + + 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 + }; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder, + uid: uid + }).yields(null, [{}]); + + imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { + message.body = body; + cb({}); + }); + + localStoreStub = sinon.stub(dao, '_localStoreMessages'); + + dao.getMessageContent({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.exist; + expect(msg).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(imapStreamStub.calledOnce).to.be.true; + expect(localStoreStub.called).to.be.false; + + done(); + }); + }); + }); + + describe('decryptMessageContent', function() { + it('should not do anything when the message is not encrypted', function(done) { + var message = { + encrypted: false + }; + + dao.decryptMessageContent({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; + expect(msg).to.equal(message); + + done(); + }); + }); + + it('should not do anything when the message is already decrypted', function(done) { + var message = { + encrypted: true, + decrypted: true + }; + + dao.decryptMessageContent({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; + expect(msg).to.equal(message); + + done(); + }); + }); + + it('decrypt a pgp/mime message', function(done) { + var message, parsedBody, mimeBody, parseStub; + + message = { + from: [{address: 'asdasdasd'}], + encrypted: true, + decrypted: false, + body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }; + + mimeBody = 'Content-Transfer-Encoding: Content-Type:'; + parsedBody = 'body? yes.'; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yields(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yields(null, mimeBody); + parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb){ + expect(o.message).to.equal(message); + expect(o.block).to.equal(mimeBody); + + o.message.body = parsedBody; + cb(null, o.message); + }); + + dao.decryptMessageContent({ + 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(parsedBody); + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.calledOnce).to.be.true; + + done(); + }); + }); + + 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).yields(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yields(null, plaintextBody); + parseStub = sinon.stub(dao, '_imapParseMessageBlock'); + + dao.decryptMessageContent({ + 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(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; + + done(); + }); + }); + + it('should fail during decryption message', function(done) { + var message, plaintextBody, parseStub, errMsg; + + message = { + from: [{address: 'asdasdasd'}], + encrypted: true, + decrypted: false, + body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }; + + plaintextBody = 'body? yes.'; + errMsg = 'yaddayadda'; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yields(null, mockKeyPair.publicKey); + pgpStub.decrypt.yields({ + errMsg: errMsg + }); + parseStub = sinon.stub(dao, '_imapParseMessageBlock'); + + dao.decryptMessageContent({ + 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(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; + + done(); + }); + }); + + it('should fail during key export', function(done) { + var message, parseStub; + + message = { + from: [{address: 'asdasdasd'}], + encrypted: true, + decrypted: false, + body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }; + + keychainStub.getReceiverPublicKey.yields({}); + parseStub = sinon.stub(dao, '_imapParseMessageBlock'); + + dao.decryptMessageContent({ + message: message + }, function(error, msg) { + expect(error).to.exist; + expect(msg).to.not.exist; + expect(message.decrypted).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.called).to.be.false; + expect(parseStub.called).to.be.false; + + done(); + }); + }); + }); + + describe('sync', function() { - it('should work initially', function(done) { - var folder, localListStub, invocations, imapSearchStub, imapParseStub; + it('should initially fill from local', function(done) { + var folder, localListStub, invocations, imapSearchStub; invocations = 0; folder = 'FOLDAAAA'; @@ -879,25 +1353,10 @@ define(function(require) { type: 'Folder', path: folder }]; - dummyDecryptedMail.unread = true; - dummyEncryptedMail.unread = true; - dummyDecryptedMail.attachments = [{ - filename: 'filename', - filesize: 123, - mimeType: 'text/plain', - content: null - }, { - filename: 'filename', - filesize: 123, - mimeType: "application/pgp-signature", - content: null - }]; localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder }).yields(null, [dummyEncryptedMail]); - keychainStub.getReceiverPublicKey.withArgs(dummyEncryptedMail.from[0].address).yields(null, mockKeyPair); - pgpStub.decrypt.withArgs(dummyEncryptedMail.body, mockKeyPair.publicKey).yields(null, dummyDecryptedMail.body); imapSearchStub = sinon.stub(dao, '_imapSearch'); imapSearchStub.withArgs({ folder: folder @@ -905,19 +1364,12 @@ define(function(require) { imapSearchStub.withArgs({ folder: folder, unread: true - }).yields(null, [dummyEncryptedMail.uid]); + }).yields(null, []); imapSearchStub.withArgs({ folder: folder, answered: true }).yields(null, []); - imapParseStub = sinon.stub(dao, '_imapParseMessageBlock'); - imapParseStub.withArgs({ - message: dummyEncryptedMail, - block: dummyDecryptedMail.body - }).yields(null, dummyDecryptedMail); - - dao.sync({ folder: folder }, function(err) { @@ -931,108 +1383,10 @@ define(function(require) { expect(dao._account.busy).to.be.false; expect(dao._account.folders[0].messages.length).to.equal(1); - expect(dao._account.folders[0].messages[0]).to.equal(dummyDecryptedMail); - expect(dao._account.folders[0].messages[0].attachments.length).to.equal(1); - expect(localListStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(dao._account.folders[0].count).to.equal(1); - expect(imapParseStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should initially error on decryption', function(done) { - var folder, localListStub, invocations; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, [dummyEncryptedMail]); - keychainStub.getReceiverPublicKey.yields({}); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.exist; - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - - done(); - }); - }); - - it('should initially sync downstream when storage is empty', function(done) { - var folder, localListStub, localStoreStub, invocations, imapSearchStub, imapGetStub, imapParseStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder - }]; - - dummyDecryptedMail.unread = true; - dummyDecryptedMail.answered = true; - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, []); - imapGetStub = sinon.stub(dao, '_imapGetMessage').withArgs({ - folder: folder, - uid: dummyEncryptedMail.uid - }).yields(null, dummyEncryptedMail); - keychainStub.getReceiverPublicKey.withArgs(dummyEncryptedMail.from[0].address).yields(null, mockKeyPair); - pgpStub.decrypt.withArgs(dummyEncryptedMail.body, mockKeyPair.publicKey).yields(null, dummyDecryptedMail.body); - - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, [dummyEncryptedMail.uid]); - - imapParseStub = sinon.stub(dao, '_imapParseMessageBlock'); - imapParseStub.yields(null, dummyDecryptedMail); - - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.not.be.empty; + expect(dao._account.folders[0].messages[0].uid).to.equal(dummyEncryptedMail.uid); + expect(dao._account.folders[0].messages[0].body).to.not.exist; expect(localListStub.calledOnce).to.be.true; expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(dao._account.folders[0].count).to.equal(1); - expect(imapParseStub.calledOnce).to.be.true; done(); }); @@ -1049,7 +1403,7 @@ define(function(require) { }); }); - it('should fetch messages downstream from the remote', function(done) { + it('should not work without providing a folder', function(done) { dao.sync({}, function(err) { expect(err).to.exist; done(); @@ -1389,8 +1743,8 @@ define(function(require) { }); }); - it('should fetch legacy messages downstream from the remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub; + it('should fetch messages downstream from the remote', function(done) { + var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; invocations = 0; folder = 'FOLDAAAA'; @@ -1400,9 +1754,12 @@ define(function(require) { messages: [] }]; + delete dummyEncryptedMail.body; + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder }).yields(null, []); + imapSearchStub = sinon.stub(dao, '_imapSearch'); imapSearchStub.withArgs({ folder: folder @@ -1416,92 +1773,18 @@ define(function(require) { answered: true }).yields(null, []); - imapGetStub = sinon.stub(dao, '_imapGetMessage').withArgs({ + imapListMessagesStub = sinon.stub(dao, '_imapListMessages'); + imapListMessagesStub.withArgs({ folder: folder, - uid: dummyEncryptedMail.uid - }).yields(null, dummyEncryptedMail); + firstUid: dummyEncryptedMail.uid, + lastUid: dummyEncryptedMail.uid + }).yields(null, [dummyEncryptedMail]); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); - keychainStub.getReceiverPublicKey.withArgs(dummyEncryptedMail.from[0].address).yields(null, mockKeyPair); - pgpStub.decrypt.withArgs(dummyEncryptedMail.body, mockKeyPair.publicKey).yields(null, dummyLegacyDecryptedMail.body); - - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.not.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - done(); - }); - }); - - it('should fetch valid pgp messages downstream from the remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub, imapParseStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - dummyDecryptedMail.attachments = [{ - filename: 'filename', - filesize: 123, - mimeType: 'text/plain', - content: null - }, { - filename: 'filename', - filesize: 123, - mimeType: "application/pgp-signature", - content: null - }]; - - - localListStub = sinon.stub(dao, '_localListMessages').withArgs({ - folder: folder - }).yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapSearchStub.withArgs({ + localStoreStub = sinon.stub(dao, '_localStoreMessages'); + localStoreStub.withArgs({ folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - - imapGetStub = sinon.stub(dao, '_imapGetMessage').withArgs({ - folder: folder, - uid: dummyEncryptedMail.uid - }).yields(null, dummyEncryptedMail); - - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); - keychainStub.getReceiverPublicKey.withArgs(dummyEncryptedMail.from[0].address).yields(null, mockKeyPair); - pgpStub.decrypt.withArgs(dummyEncryptedMail.body, mockKeyPair.publicKey).yields(null, dummyDecryptedMail.body); - - imapParseStub = sinon.stub(dao, '_imapParseMessageBlock'); - imapParseStub.withArgs({ - message: dummyEncryptedMail, - block: dummyDecryptedMail.body - }).yields(null, dummyDecryptedMail); + emails: [dummyEncryptedMail] + }).yields(); dao.sync({ folder: folder @@ -1516,21 +1799,17 @@ define(function(require) { expect(dao._account.busy).to.be.false; expect(dao._account.folders[0].messages.length).to.equal(1); - expect(dao._account.folders[0].messages[0]).to.equal(dummyDecryptedMail); - expect(dao._account.folders[0].messages[0].attachments.length).to.equal(1); + expect(dao._account.folders[0].messages[0].uid).to.equal(dummyEncryptedMail.uid); + expect(dao._account.folders[0].messages[0].body).to.not.exist; expect(localListStub.calledOnce).to.be.true; expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(imapParseStub.calledOnce).to.be.true; done(); }); }); it('should not fetch non-whitelisted mails', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub; + var invocations, folder, localListStub, imapSearchStub, imapListMessagesStub, localStoreStub; invocations = 0; folder = 'FOLDAAAA'; @@ -1553,7 +1832,14 @@ define(function(require) { folder: folder, answered: true }).yields(null, []); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, nonWhitelistedMail); + + imapListMessagesStub = sinon.stub(dao, '_imapListMessages'); + imapListMessagesStub.withArgs({ + folder: folder, + firstUid: nonWhitelistedMail.uid, + lastUid: nonWhitelistedMail.uid + }).yields(null, [nonWhitelistedMail]); + localStoreStub = sinon.stub(dao, '_localStoreMessages'); dao.sync({ @@ -1571,60 +1857,13 @@ define(function(require) { expect(dao._account.folders[0].messages).to.be.empty; expect(localListStub.calledOnce).to.be.true; expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; expect(localStoreStub.called).to.be.false; - expect(keychainStub.getReceiverPublicKey.called).to.be.false; - expect(pgpStub.decrypt.called).to.be.false; - done(); - }); - }); - - it('should error while decrypting fetch messages from the remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, dummyEncryptedMail); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); - keychainStub.getReceiverPublicKey.yields({}); - - dao.sync({ - folder: folder - }, function(err) { - - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.called).to.be.false; done(); }); }); it('should error while storing messages from the remote locally', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub; + var invocations, folder, localListStub, imapSearchStub, localStoreStub, imapListMessagesStub; invocations = 0; folder = 'FOLDAAAA'; @@ -1634,56 +1873,20 @@ define(function(require) { messages: [] }]; - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + delete dummyEncryptedMail.body; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder + }).yields(null, []); + imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, dummyEncryptedMail); - localStoreStub = sinon.stub(dao, '_localStoreMessages').yields({}); + imapSearchStub.yields(null, [dummyEncryptedMail.uid]); - dao.sync({ - folder: folder - }, function(err) { + imapListMessagesStub = sinon.stub(dao, '_imapListMessages'); + imapListMessagesStub.yields(null, [dummyEncryptedMail]); - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.called).to.be.false; - expect(pgpStub.decrypt.called).to.be.false; - done(); - }); - }); - - it('should error while fetching messages from the remote', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [dummyEncryptedMail.uid]); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields({}); localStoreStub = sinon.stub(dao, '_localStoreMessages'); + localStoreStub.yields({}); dao.sync({ folder: folder @@ -1698,19 +1901,16 @@ define(function(require) { expect(err).to.exist; expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; + expect(dao._account.folders[0].messages.length).to.equal(0); expect(localListStub.calledOnce).to.be.true; expect(imapSearchStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.called).to.be.false; - expect(keychainStub.getReceiverPublicKey.called).to.be.false; - expect(pgpStub.decrypt.called).to.be.false; + expect(localStoreStub.calledOnce).to.be.true; done(); }); }); it('should verify an authentication mail', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, markReadStub, imapDeleteStub; + var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub; invocations = 0; folder = 'FOLDAAAA'; @@ -1721,6 +1921,7 @@ define(function(require) { }]; localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapSearchStub = sinon.stub(dao, '_imapSearch'); imapSearchStub.withArgs({ folder: folder @@ -1733,14 +1934,9 @@ define(function(require) { folder: folder, answered: true }).yields(null, []); - - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); - markReadStub = sinon.stub(dao, '_imapMark').withArgs({ - folder: folder, - uid: verificationMail.uid, - unread: false - }).yields(); imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').withArgs({ folder: folder, uid: verificationMail.uid @@ -1763,7 +1959,6 @@ define(function(require) { expect(imapSearchStub.calledThrice).to.be.true; expect(imapGetStub.calledOnce).to.be.true; expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(markReadStub.calledOnce).to.be.true; expect(imapDeleteStub.calledOnce).to.be.true; done(); @@ -1771,8 +1966,7 @@ define(function(require) { }); it('should fail during deletion of an authentication mail', function(done) { - var invocations, folder, localListStub, imapSearchStub, - imapGetStub, markReadStub, imapDeleteStub; + var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub; invocations = 0; folder = 'FOLDAAAA'; @@ -1783,148 +1977,7 @@ define(function(require) { }]; localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); - keychainStub.verifyPublicKey.yields(); - markReadStub = sinon.stub(dao, '_imapMark').yields(); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); - - dao.sync({ - folder: folder - }, function(err) { - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(markReadStub.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail during marking an authentication mail read', function(done) { - var invocations, folder, localListStub, imapSearchStub, - imapGetStub, markReadStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); - keychainStub.verifyPublicKey.yields(); - markReadStub = sinon.stub(dao, '_imapMark').yields({}); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); - - dao.sync({ - folder: folder - }, function(err) { - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(markReadStub.calledOnce).to.be.true; - expect(imapDeleteStub.called).to.be.false; - - done(); - }); - }); - - it('should fail during verifying authentication', function(done) { - var invocations, folder, localListStub, imapSearchStub, - imapGetStub, markReadStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); - keychainStub.verifyPublicKey.yields({}); - markReadStub = sinon.stub(dao, '_imapMark'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); - - dao.sync({ - folder: folder - }, function(err) { - if (invocations === 0) { - expect(err).to.not.exist; - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(err).to.exist; - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(markReadStub.called).to.be.false; - expect(imapDeleteStub.called).to.be.false; - - done(); - }); - }); - - it('should not bother about read authentication mails', function(done) { - var invocations, folder, localListStub, imapSearchStub, - imapGetStub, markReadStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - verificationMail.unread = false; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); imapSearchStub = sinon.stub(dao, '_imapSearch'); imapSearchStub.withArgs({ folder: folder @@ -1937,13 +1990,15 @@ define(function(require) { folder: folder, answered: true }).yields(null, []); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); - markReadStub = sinon.stub(dao, '_imapMark'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); + keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); dao.sync({ folder: folder }, function(err) { + if (invocations === 0) { expect(err).to.not.exist; expect(dao._account.busy).to.be.true; @@ -1951,22 +2006,21 @@ define(function(require) { return; } - expect(err).to.not.exist; + expect(err).to.exist; expect(dao._account.busy).to.be.false; expect(dao._account.folders[0].messages).to.be.empty; expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; + expect(imapSearchStub.calledOnce).to.be.true; expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.called).to.be.false; - expect(markReadStub.called).to.be.false; - expect(imapDeleteStub.called).to.be.false; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; done(); }); }); - it('should not bother about corrupted authentication mails', function(done) { - var invocations, folder, localListStub, imapSearchStub, imapGetStub, markReadStub, imapDeleteStub; + it('should fail during verifying authentication', function(done) { + var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub; invocations = 0; folder = 'FOLDAAAA'; @@ -1977,6 +2031,63 @@ define(function(require) { }]; localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + + imapSearchStub = sinon.stub(dao, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [verificationMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); + keychainStub.verifyPublicKey.withArgs(verificationUuid).yields({ + errMsg: 'fubar' + }); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); + + dao.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.called).to.be.false; + + done(); + }); + }); + + it('should not bother about corrupted authentication mails', function(done) { + var invocations, folder, localListStub, imapSearchStub, imapGetStub, imapListMessagesStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapSearchStub = sinon.stub(dao, '_imapSearch'); imapSearchStub.withArgs({ folder: folder @@ -1989,65 +2100,12 @@ define(function(require) { folder: folder, answered: true }).yields(null, []); - - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, corruptedVerificationMail); - markReadStub = sinon.stub(dao, '_imapMark'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); - - dao.sync({ - folder: folder - }, function(err) { - expect(err).to.not.exist; - - if (invocations === 0) { - expect(dao._account.busy).to.be.true; - invocations++; - return; - } - - expect(dao._account.busy).to.be.false; - expect(dao._account.folders[0].messages).to.be.empty; - expect(localListStub.calledOnce).to.be.true; - expect(imapSearchStub.calledThrice).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.called).to.be.false; - expect(markReadStub.called).to.be.false; - expect(imapDeleteStub.called).to.be.false; - - done(); + imapListMessagesStub = sinon.stub(dao, '_imapListMessages').yields(null, [corruptedVerificationMail]); + imapGetStub = sinon.stub(dao, '_imapStreamText').yields(null); + keychainStub.verifyPublicKey.withArgs(corruptedVerificationUuid).yields({ + errMsg: 'fubar' }); - }); - - it('should not bother about corrupted authentication mails no verification link', function(done) { - var invocations, folder, localListStub, imapSearchStub, - imapGetStub, markReadStub, imapDeleteStub; - - invocations = 0; - folder = 'FOLDAAAA'; - dao._account.folders = [{ - type: 'Folder', - path: folder, - messages: [] - }]; - - verificationMail.body = 'url? there is no url.'; - - localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); - imapSearchStub = sinon.stub(dao, '_imapSearch'); - imapSearchStub.withArgs({ - folder: folder - }).yields(null, [verificationMail.uid]); - imapSearchStub.withArgs({ - folder: folder, - unread: true - }).yields(null, []); - imapSearchStub.withArgs({ - folder: folder, - answered: true - }).yields(null, []); - imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); - markReadStub = sinon.stub(dao, '_imapMark'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); dao.sync({ folder: folder @@ -2066,7 +2124,6 @@ define(function(require) { expect(imapSearchStub.calledThrice).to.be.true; expect(imapGetStub.calledOnce).to.be.true; expect(keychainStub.verifyPublicKey.called).to.be.false; - expect(markReadStub.called).to.be.false; expect(imapDeleteStub.called).to.be.false; done();