From 0e9f68abeef6db9214e946b1537c052179658653 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Fri, 14 Feb 2014 17:29:16 +0100 Subject: [PATCH 01/10] 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(); From 250aa4b886978d645bcdfdf8c37f0d60ae9f51bb Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Mon, 17 Feb 2014 14:31:14 +0100 Subject: [PATCH 02/10] adapt ui to async content fetching --- src/js/controller/mail-list.js | 132 ++++++++++++++++++++++++++- src/js/dao/email-dao.js | 30 ++++-- src/lib/iscroll/iscroll-min.js | 4 +- src/tpl/mail-list.html | 2 +- test/new-unit/email-dao-test.js | 84 +++++++++++------ test/new-unit/mail-list-ctrl-test.js | 68 ++++++++++++++ 6 files changed, 277 insertions(+), 43 deletions(-) diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 855a4ce..7704bdc 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -58,6 +58,16 @@ define(function(require) { // scope functions // + $scope.getContent = function(email) { + emailDao.getMessageContent({ + folder: getFolder().path, + message: email + }, function(error) { + $scope.$apply(); + $scope.onError(error); + }); + }; + /** * Called when clicking on an email list item */ @@ -67,6 +77,13 @@ define(function(require) { return; } + emailDao.decryptMessageContent({ + message: email + }, function(error) { + $scope.$apply(); + $scope.onError(error); + }); + $scope.state.mailList.selected = email; $scope.state.read.toggle(true); @@ -279,10 +296,88 @@ define(function(require) { }]; // list of receivers if (attachments) { // body structure with three attachments - this.bodystructure = {"1": {"part": "1","type": "text/plain","parameters": {"charset": "us-ascii"},"encoding": "7bit","size": 9,"lines": 2},"2": {"part": "2","type": "application/octet-stream","parameters": {"name": "a.md"},"encoding": "7bit","size": 123,"disposition": [{"type": "attachment","filename": "a.md"}]},"3": {"part": "3","type": "application/octet-stream","parameters": {"name": "b.md"},"encoding": "7bit","size": 456,"disposition": [{"type": "attachment","filename": "b.md"}]},"4": {"part": "4","type": "application/octet-stream","parameters": {"name": "c.md"},"encoding": "7bit","size": 789,"disposition": [{"type": "attachment","filename": "c.md"}]},"type": "multipart/mixed"}; - this.attachments = [{"filename": "a.md","filesize": 123,"mimeType": "text/x-markdown","part": "2","content": null}, {"filename": "b.md","filesize": 456,"mimeType": "text/x-markdown","part": "3","content": null}, {"filename": "c.md","filesize": 789,"mimeType": "text/x-markdown","part": "4","content": null}]; + this.bodystructure = { + "1": { + "part": "1", + "type": "text/plain", + "parameters": { + "charset": "us-ascii" + }, + "encoding": "7bit", + "size": 9, + "lines": 2 + }, + "2": { + "part": "2", + "type": "application/octet-stream", + "parameters": { + "name": "a.md" + }, + "encoding": "7bit", + "size": 123, + "disposition": [{ + "type": "attachment", + "filename": "a.md" + }] + }, + "3": { + "part": "3", + "type": "application/octet-stream", + "parameters": { + "name": "b.md" + }, + "encoding": "7bit", + "size": 456, + "disposition": [{ + "type": "attachment", + "filename": "b.md" + }] + }, + "4": { + "part": "4", + "type": "application/octet-stream", + "parameters": { + "name": "c.md" + }, + "encoding": "7bit", + "size": 789, + "disposition": [{ + "type": "attachment", + "filename": "c.md" + }] + }, + "type": "multipart/mixed" + }; + this.attachments = [{ + "filename": "a.md", + "filesize": 123, + "mimeType": "text/x-markdown", + "part": "2", + "content": null + }, { + "filename": "b.md", + "filesize": 456, + "mimeType": "text/x-markdown", + "part": "3", + "content": null + }, { + "filename": "c.md", + "filesize": 789, + "mimeType": "text/x-markdown", + "part": "4", + "content": null + }]; } else { - this.bodystructure = {"part": "1","type": "text/plain","parameters": {"charset": "us-ascii"},"encoding": "7bit","size": 9,"lines": 2}; + this.bodystructure = { + "part": "1", + "type": "text/plain", + "parameters": { + "charset": "us-ascii" + }, + "encoding": "7bit", + "size": 9, + "lines": 2 + }; this.attachments = []; } this.unread = unread; @@ -311,8 +406,37 @@ define(function(require) { var myScroll; // activate iscroll myScroll = new IScroll(elm[0], { - mouseWheel: true + mouseWheel: true, }); + + // load the visible message bodies, when the list is re-initialized and when scrolling stopped + loadVisible(); + myScroll.on('scrollEnd', loadVisible); + + function loadVisible() { + var list = elm[0].getBoundingClientRect(), + footerHeight = elm[0].nextElementSibling.getBoundingClientRect().height, + top = list.top, + bottom = list.bottom - footerHeight, + listItems = elm[0].children[0].children, + i = listItems.length, + listItem, message, + isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible; + + while (i--) { + listItem = listItems.item(i).getBoundingClientRect(); + message = scope.filteredMessages[i]; + + isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top + isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom + isVisible = listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole + + + if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) { + scope.getContent(message); + } + } + } }, true); } }; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 5eec539..04944d9 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -742,12 +742,17 @@ define(function(require) { message = options.message, folder = options.folder; - // the message already has a body, so no need to become active here - if (message.body) { - callback(null, message); + if (message.loadingBody) { return; } + // the message already has a body, so no need to become active here + if (message.body) { + return; + } + + message.loadingBody = true; + // the mail does not have its content in memory readFromDevice(); @@ -760,6 +765,7 @@ define(function(require) { var localMessage; if (err) { + message.loadingBody = false; callback(err); return; } @@ -785,10 +791,13 @@ define(function(require) { message: message }, function(error) { if (error) { + message.loadingBody = false; callback(error); return; } + message.loadingBody = false; + self._localStoreMessages({ folder: folder, emails: [message] @@ -813,6 +822,7 @@ define(function(require) { message.decrypted = false; extractCiphertext(); } + message.loadingBody = false; callback(null, message); } @@ -834,15 +844,17 @@ define(function(require) { var self = this, message = options.message; - // the message is not encrypted or has already been decrypted - if (!message.encrypted || message.decrypted) { - callback(null, message); + // the message has no body, is not encrypted or has already been decrypted + if (message.decryptingBody || !message.body || !message.encrypted || message.decrypted) { return; } + message.decryptingBody = true; + // 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); return; } @@ -850,6 +862,7 @@ 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); return; } @@ -863,6 +876,7 @@ define(function(require) { if (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && decrypted.indexOf('Content-Type:') === -1) { message.body = decrypted; message.decrypted = true; + message.decryptingBody = false; callback(null, message); return; } @@ -873,6 +887,7 @@ define(function(require) { block: decrypted }, function(error) { if (error) { + message.decryptingBody = false; callback(error); return; } @@ -885,7 +900,8 @@ define(function(require) { }); // we're done here! - callback(error, message); + message.decryptingBody = false; + callback(null, message); }); }); }); diff --git a/src/lib/iscroll/iscroll-min.js b/src/lib/iscroll/iscroll-min.js index 24c5464..c608784 100644 --- a/src/lib/iscroll/iscroll-min.js +++ b/src/lib/iscroll/iscroll-min.js @@ -1,2 +1,2 @@ -/*! iScroll v5.0.5 ~ (c) 2008-2013 Matteo Spinelli ~ http://cubiq.org/license */ -var IScroll=function(t,i,s){function e(t,s){this.wrapper="string"==typeof t?i.querySelector(t):t,this.scroller=this.wrapper.children[0],this.scrollerStyle=this.scroller.style,this.options={resizeIndicator:!0,mouseWheelSpeed:20,snapThreshold:.334,startX:0,startY:0,scrollY:!0,directionLockThreshold:5,momentum:!0,bounce:!0,bounceTime:600,bounceEasing:"",preventDefault:!0,preventDefaultException:{tagName:/^(INPUT|TEXTAREA|BUTTON|SELECT)$/},HWCompositing:!0,useTransition:!0,useTransform:!0};for(var e in s)this.options[e]=s[e];this.translateZ=this.options.HWCompositing&&h.hasPerspective?" translateZ(0)":"",this.options.useTransition=h.hasTransition&&this.options.useTransition,this.options.useTransform=h.hasTransform&&this.options.useTransform,this.options.eventPassthrough=this.options.eventPassthrough===!0?"vertical":this.options.eventPassthrough,this.options.preventDefault=!this.options.eventPassthrough&&this.options.preventDefault,this.options.scrollY="vertical"==this.options.eventPassthrough?!1:this.options.scrollY,this.options.scrollX="horizontal"==this.options.eventPassthrough?!1:this.options.scrollX,this.options.freeScroll=this.options.freeScroll&&!this.options.eventPassthrough,this.options.directionLockThreshold=this.options.eventPassthrough?0:this.options.directionLockThreshold,this.options.bounceEasing="string"==typeof this.options.bounceEasing?h.ease[this.options.bounceEasing]||h.ease.circular:this.options.bounceEasing,this.options.resizePolling=void 0===this.options.resizePolling?60:this.options.resizePolling,this.options.tap===!0&&(this.options.tap="tap"),this.options.invertWheelDirection=this.options.invertWheelDirection?-1:1,this.x=0,this.y=0,this.directionX=0,this.directionY=0,this._events={},this._init(),this.refresh(),this.scrollTo(this.options.startX,this.options.startY),this.enable()}function o(t,s,e){var o=i.createElement("div"),n=i.createElement("div");return e===!0&&(o.style.cssText="position:absolute;z-index:9999",n.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px"),n.className="iScrollIndicator","h"==t?(e===!0&&(o.style.cssText+=";height:7px;left:2px;right:2px;bottom:0",n.style.height="100%"),o.className="iScrollHorizontalScrollbar"):(e===!0&&(o.style.cssText+=";width:7px;bottom:2px;top:2px;right:1px",n.style.width="100%"),o.className="iScrollVerticalScrollbar"),s||(o.style.pointerEvents="none"),o.appendChild(n),o}function n(s,e){this.wrapper="string"==typeof e.el?i.querySelector(e.el):e.el,this.indicator=this.wrapper.children[0],this.indicatorStyle=this.indicator.style,this.scroller=s,this.options={listenX:!0,listenY:!0,interactive:!1,resize:!0,defaultScrollbars:!1,speedRatioX:0,speedRatioY:0};for(var o in e)this.options[o]=e[o];this.sizeRatioX=1,this.sizeRatioY=1,this.maxPosX=0,this.maxPosY=0,this.options.interactive&&(h.addEvent(this.indicator,"touchstart",this),h.addEvent(this.indicator,"MSPointerDown",this),h.addEvent(this.indicator,"mousedown",this),h.addEvent(t,"touchend",this),h.addEvent(t,"MSPointerUp",this),h.addEvent(t,"mouseup",this))}var r=t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||t.msRequestAnimationFrame||function(i){t.setTimeout(i,1e3/60)},h=function(){function e(t){return r===!1?!1:""===r?t:r+t.charAt(0).toUpperCase()+t.substr(1)}var o={},n=i.createElement("div").style,r=function(){for(var t,i=["t","webkitT","MozT","msT","OT"],s=0,e=i.length;e>s;s++)if(t=i[s]+"ransform",t in n)return i[s].substr(0,i[s].length-1);return!1}();o.getTime=Date.now||function(){return(new Date).getTime()},o.extend=function(t,i){for(var s in i)t[s]=i[s]},o.addEvent=function(t,i,s,e){t.addEventListener(i,s,!!e)},o.removeEvent=function(t,i,s,e){t.removeEventListener(i,s,!!e)},o.momentum=function(t,i,e,o,n){var r,h,a=t-i,l=s.abs(a)/e,c=6e-4;return r=t+l*l/(2*c)*(0>a?-1:1),h=l/c,o>r?(r=n?o-n/2.5*(l/8):o,a=s.abs(r-t),h=a/l):r>0&&(r=n?n/2.5*(l/8):0,a=s.abs(t)+r,h=a/l),{destination:s.round(r),duration:h}};var h=e("transform");return o.extend(o,{hasTransform:h!==!1,hasPerspective:e("perspective")in n,hasTouch:"ontouchstart"in t,hasPointer:navigator.msPointerEnabled,hasTransition:e("transition")in n}),o.isAndroidBrowser=/Android/.test(t.navigator.appVersion)&&/Version\/\d/.test(t.navigator.appVersion),o.extend(o.style={},{transform:h,transitionTimingFunction:e("transitionTimingFunction"),transitionDuration:e("transitionDuration"),transformOrigin:e("transformOrigin")}),o.hasClass=function(t,i){var s=new RegExp("(^|\\s)"+i+"(\\s|$)");return s.test(t.className)},o.addClass=function(t,i){if(!o.hasClass(t,i)){var s=t.className.split(" ");s.push(i),t.className=s.join(" ")}},o.removeClass=function(t,i){if(o.hasClass(t,i)){var s=new RegExp("(^|\\s)"+i+"(\\s|$)","g");t.className=t.className.replace(s," ")}},o.offset=function(t){for(var i=-t.offsetLeft,s=-t.offsetTop;t=t.offsetParent;)i-=t.offsetLeft,s-=t.offsetTop;return{left:i,top:s}},o.preventDefaultException=function(t,i){for(var s in i)if(i[s].test(t[s]))return!0;return!1},o.extend(o.eventType={},{touchstart:1,touchmove:1,touchend:1,mousedown:2,mousemove:2,mouseup:2,MSPointerDown:3,MSPointerMove:3,MSPointerUp:3}),o.extend(o.ease={},{quadratic:{style:"cubic-bezier(0.25, 0.46, 0.45, 0.94)",fn:function(t){return t*(2-t)}},circular:{style:"cubic-bezier(0.1, 0.57, 0.1, 1)",fn:function(t){return s.sqrt(1- --t*t)}},back:{style:"cubic-bezier(0.175, 0.885, 0.32, 1.275)",fn:function(t){var i=4;return(t-=1)*t*((i+1)*t+i)+1}},bounce:{style:"",fn:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?7.5625*(t-=1.5/2.75)*t+.75:2.5/2.75>t?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375}},elastic:{style:"",fn:function(t){var i=.22,e=.4;return 0===t?0:1==t?1:e*s.pow(2,-10*t)*s.sin((t-i/4)*2*s.PI/i)+1}}}),o.tap=function(t,s){var e=i.createEvent("Event");e.initEvent(s,!0,!0),e.pageX=t.pageX,e.pageY=t.pageY,t.target.dispatchEvent(e)},o.click=function(t){var s,e=t.target;"SELECT"!=e.tagName&&"INPUT"!=e.tagName&&"TEXTAREA"!=e.tagName&&(s=i.createEvent("MouseEvents"),s.initMouseEvent("click",!0,!0,t.view,1,e.screenX,e.screenY,e.clientX,e.clientY,t.ctrlKey,t.altKey,t.shiftKey,t.metaKey,0,null),s._constructed=!0,e.dispatchEvent(s))},o}();return e.prototype={version:"5.0.5",_init:function(){this._initEvents(),(this.options.scrollbars||this.options.indicators)&&this._initIndicators(),this.options.mouseWheel&&this._initWheel(),this.options.snap&&this._initSnap(),this.options.keyBindings&&this._initKeys()},destroy:function(){this._initEvents(!0),this._execEvent("destroy")},_transitionEnd:function(t){t.target==this.scroller&&(this._transitionTime(0),this.resetPosition(this.options.bounceTime)||this._execEvent("scrollEnd"))},_start:function(t){if(!(1!=h.eventType[t.type]&&0!==t.button||!this.enabled||this.initiated&&h.eventType[t.type]!==this.initiated)){!this.options.preventDefault||h.isAndroidBrowser||h.preventDefaultException(t.target,this.options.preventDefaultException)||t.preventDefault();var i,e=t.touches?t.touches[0]:t;this.initiated=h.eventType[t.type],this.moved=!1,this.distX=0,this.distY=0,this.directionX=0,this.directionY=0,this.directionLocked=0,this._transitionTime(),this.isAnimating=!1,this.startTime=h.getTime(),this.options.useTransition&&this.isInTransition&&(i=this.getComputedPosition(),this._translate(s.round(i.x),s.round(i.y)),this.isInTransition=!1),this.startX=this.x,this.startY=this.y,this.absStartX=this.x,this.absStartY=this.y,this.pointX=e.pageX,this.pointY=e.pageY,this._execEvent("scrollStart")}},_move:function(t){if(this.enabled&&h.eventType[t.type]===this.initiated){this.options.preventDefault&&t.preventDefault();var i,e,o,n,r=t.touches?t.touches[0]:t,a=this.hasHorizontalScroll?r.pageX-this.pointX:0,l=this.hasVerticalScroll?r.pageY-this.pointY:0,c=h.getTime();if(this.pointX=r.pageX,this.pointY=r.pageY,this.distX+=a,this.distY+=l,o=s.abs(this.distX),n=s.abs(this.distY),!(c-this.endTime>300&&10>o&&10>n)){if(this.directionLocked||this.options.freeScroll||(this.directionLocked=o>n+this.options.directionLockThreshold?"h":n>=o+this.options.directionLockThreshold?"v":"n"),"h"==this.directionLocked){if("vertical"==this.options.eventPassthrough)t.preventDefault();else if("horizontal"==this.options.eventPassthrough)return this.initiated=!1,void 0;l=0}else if("v"==this.directionLocked){if("horizontal"==this.options.eventPassthrough)t.preventDefault();else if("vertical"==this.options.eventPassthrough)return this.initiated=!1,void 0;a=0}i=this.x+a,e=this.y+l,(i>0||i0?0:this.maxScrollX),(e>0||e0?0:this.maxScrollY),this.directionX=a>0?-1:0>a?1:0,this.directionY=l>0?-1:0>l?1:0,this.moved=!0,this._translate(i,e),c-this.startTime>300&&(this.startTime=c,this.startX=this.x,this.startY=this.y)}}},_end:function(t){if(this.enabled&&h.eventType[t.type]===this.initiated){this.options.preventDefault&&!h.preventDefaultException(t.target,this.options.preventDefaultException)&&t.preventDefault();var i,e,o=(t.changedTouches?t.changedTouches[0]:t,h.getTime()-this.startTime),n=s.round(this.x),r=s.round(this.y),a=s.abs(n-this.startX),l=s.abs(r-this.startY),c=0,p="";if(this.scrollTo(n,r),this.isInTransition=0,this.initiated=0,this.endTime=h.getTime(),!this.resetPosition(this.options.bounceTime)){if(!this.moved)return this.options.tap&&h.tap(t,this.options.tap),this.options.click&&h.click(t),void 0;if(this._events.flick&&200>o&&100>a&&100>l)return this._execEvent("flick"),void 0;if(this.options.momentum&&300>o&&(i=this.hasHorizontalScroll?h.momentum(this.x,this.startX,o,this.maxScrollX,this.options.bounce?this.wrapperWidth:0):{destination:n,duration:0},e=this.hasVerticalScroll?h.momentum(this.y,this.startY,o,this.maxScrollY,this.options.bounce?this.wrapperHeight:0):{destination:r,duration:0},n=i.destination,r=e.destination,c=s.max(i.duration,e.duration),this.isInTransition=1),this.options.snap){var d=this._nearestSnap(n,r);this.currentPage=d,c=this.options.snapSpeed||s.max(s.max(s.min(s.abs(n-d.x),1e3),s.min(s.abs(r-d.y),1e3)),300),n=d.x,r=d.y,this.directionX=0,this.directionY=0,p=this.options.bounceEasing}return n!=this.x||r!=this.y?((n>0||n0||r0?i=0:this.x0?s=0:this.yi;i++)this._events[t][i].call(this)}},scrollBy:function(t,i,s,e){t=this.x+t,i=this.y+i,s=s||0,this.scrollTo(t,i,s,e)},scrollTo:function(t,i,s,e){e=e||h.ease.circular,!s||this.options.useTransition&&e.style?(this._transitionTimingFunction(e.style),this._transitionTime(s),this._translate(t,i)):this._animate(t,i,s,e.fn)},scrollToElement:function(t,i,e,o,n){if(t=t.nodeType?t:this.scroller.querySelector(t)){var r=h.offset(t);r.left-=this.wrapperOffset.left,r.top-=this.wrapperOffset.top,e===!0&&(e=s.round(t.offsetWidth/2-this.wrapper.offsetWidth/2)),o===!0&&(o=s.round(t.offsetHeight/2-this.wrapper.offsetHeight/2)),r.left-=e||0,r.top-=o||0,r.left=r.left>0?0:r.left0?0:r.top0?e--:0>i&&e++,s>0?o--:0>s&&o++,this.goToPage(e,o),void 0;e=this.x+(this.hasHorizontalScroll?i*this.options.invertWheelDirection:0),o=this.y+(this.hasVerticalScroll?s*this.options.invertWheelDirection:0),e>0?e=0:e0?o=0:o-this.scrollerWidth;){for(this.pages[h]=[],t=0,n=0;n>-this.scrollerHeight;)this.pages[h][t]={x:s.max(l,this.maxScrollX),y:s.max(n,this.maxScrollY),width:c,height:p,cx:l-e,cy:n-o},n-=p,t++;l-=c,h++}else for(r=this.options.snap,t=r.length,i=-1;t>h;h++)(0===h||r[h].offsetLeft<=r[h-1].offsetLeft)&&(a=0,i++),this.pages[a]||(this.pages[a]=[]),l=s.max(-r[h].offsetLeft,this.maxScrollX),n=s.max(-r[h].offsetTop,this.maxScrollY),e=l-s.round(r[h].offsetWidth/2),o=n-s.round(r[h].offsetHeight/2),this.pages[a][i]={x:l,y:n,width:r[h].offsetWidth,height:r[h].offsetHeight,cx:e,cy:o},l>this.maxScrollX&&a++;this.goToPage(this.currentPage.pageX||0,this.currentPage.pageY||0,0),0===this.options.snapThreshold%1?(this.snapThresholdX=this.options.snapThreshold,this.snapThresholdY=this.options.snapThreshold):(this.snapThresholdX=s.round(this.pages[this.currentPage.pageX][this.currentPage.pageY].width*this.options.snapThreshold),this.snapThresholdY=s.round(this.pages[this.currentPage.pageX][this.currentPage.pageY].height*this.options.snapThreshold))}}),this.on("flick",function(){var t=this.options.snapSpeed||s.max(s.max(s.min(s.abs(this.x-this.startX),1e3),s.min(s.abs(this.y-this.startY),1e3)),300);this.goToPage(this.currentPage.pageX+this.directionX,this.currentPage.pageY+this.directionY,t)})},_nearestSnap:function(t,i){if(!this.pages.length)return{x:0,y:0,pageX:0,pageY:0};var e=0,o=this.pages.length,n=0;if(s.abs(t-this.absStartX)0?t=0:t0?i=0:ie;e++)if(t>=this.pages[e][0].cx){t=this.pages[e][0].x;break}for(o=this.pages[e].length;o>n;n++)if(i>=this.pages[0][n].cy){i=this.pages[0][n].y;break}return e==this.currentPage.pageX&&(e+=this.directionX,0>e?e=0:e>=this.pages.length&&(e=this.pages.length-1),t=this.pages[e][0].x),n==this.currentPage.pageY&&(n+=this.directionY,0>n?n=0:n>=this.pages[0].length&&(n=this.pages[0].length-1),i=this.pages[0][n].y),{x:t,y:i,pageX:e,pageY:n}},goToPage:function(t,i,e,o){o=o||this.options.bounceEasing,t>=this.pages.length?t=this.pages.length-1:0>t&&(t=0),i>=this.pages[t].length?i=this.pages[t].length-1:0>i&&(i=0);var n=this.pages[t][i].x,r=this.pages[t][i].y;e=void 0===e?this.options.snapSpeed||s.max(s.max(s.min(s.abs(n-this.x),1e3),s.min(s.abs(r-this.y),1e3)),300):e,this.currentPage={x:n,y:r,pageX:t,pageY:i},this.scrollTo(n,r,e,o)},next:function(t,i){var s=this.currentPage.pageX,e=this.currentPage.pageY;s++,s>=this.pages.length&&this.hasVerticalScroll&&(s=0,e++),this.goToPage(s,e,t,i)},prev:function(t,i){var s=this.currentPage.pageX,e=this.currentPage.pageY;s--,0>s&&this.hasVerticalScroll&&(s=0,e--),this.goToPage(s,e,t,i)},_initKeys:function(){var i,s={pageUp:33,pageDown:34,end:35,home:36,left:37,up:38,right:39,down:40};if("object"==typeof this.options.keyBindings)for(i in this.options.keyBindings)"string"==typeof this.options.keyBindings[i]&&(this.options.keyBindings[i]=this.options.keyBindings[i].toUpperCase().charCodeAt(0));else this.options.keyBindings={};for(i in s)this.options.keyBindings[i]=this.options.keyBindings[i]||s[i];h.addEvent(t,"keydown",this),this.on("destroy",function(){h.removeEvent(t,"keydown",this)})},_key:function(t){if(this.enabled){var i,e=this.options.snap,o=e?this.currentPage.pageX:this.x,n=e?this.currentPage.pageY:this.y,r=h.getTime(),a=this.keyTime||0,l=.25;switch(this.options.useTransition&&this.isInTransition&&(i=this.getComputedPosition(),this._translate(s.round(i.x),s.round(i.y)),this.isInTransition=!1),this.keyAcceleration=200>r-a?s.min(this.keyAcceleration+l,50):0,t.keyCode){case this.options.keyBindings.pageUp:this.hasHorizontalScroll&&!this.hasVerticalScroll?o+=e?1:this.wrapperWidth:n+=e?1:this.wrapperHeight;break;case this.options.keyBindings.pageDown:this.hasHorizontalScroll&&!this.hasVerticalScroll?o-=e?1:this.wrapperWidth:n-=e?1:this.wrapperHeight;break;case this.options.keyBindings.end:o=e?this.pages.length-1:this.maxScrollX,n=e?this.pages[0].length-1:this.maxScrollY;break;case this.options.keyBindings.home:o=0,n=0;break;case this.options.keyBindings.left:o+=e?-1:5+this.keyAcceleration>>0;break;case this.options.keyBindings.up:n+=e?1:5+this.keyAcceleration>>0;break;case this.options.keyBindings.right:o-=e?-1:5+this.keyAcceleration>>0;break;case this.options.keyBindings.down:n-=e?1:5+this.keyAcceleration>>0}if(e)return this.goToPage(o,n),void 0;o>0?(o=0,this.keyAcceleration=0):o0?(n=0,this.keyAcceleration=0):n=p?(n.isAnimating=!1,n._translate(t,i),n.resetPosition(n.options.bounceTime)||n._execEvent("scrollEnd"),void 0):(g=(g-c)/s,m=e(g),d=(t-a)*m+a,u=(i-l)*m+l,n._translate(d,u),n.isAnimating&&r(o),void 0)}var n=this,a=this.x,l=this.y,c=h.getTime(),p=c+s;this.isAnimating=!0,o()},handleEvent:function(t){switch(t.type){case"touchstart":case"MSPointerDown":case"mousedown":this._start(t);break;case"touchmove":case"MSPointerMove":case"mousemove":this._move(t);break;case"touchend":case"MSPointerUp":case"mouseup":case"touchcancel":case"MSPointerCancel":case"mousecancel":this._end(t);break;case"orientationchange":case"resize":this._resize();break;case"transitionend":case"webkitTransitionEnd":case"oTransitionEnd":case"MSTransitionEnd":this._transitionEnd(t);break;case"DOMMouseScroll":case"mousewheel":this._wheel(t);break;case"keydown":this._key(t)}}},n.prototype={handleEvent:function(t){switch(t.type){case"touchstart":case"MSPointerDown":case"mousedown":this._start(t);break;case"touchmove":case"MSPointerMove":case"mousemove":this._move(t);break;case"touchend":case"MSPointerUp":case"mouseup":case"touchcancel":case"MSPointerCancel":case"mousecancel":this._end(t)}},destroy:function(){this.options.interactive&&(h.removeEvent(this.indicator,"touchstart",this),h.removeEvent(this.indicator,"MSPointerDown",this),h.removeEvent(this.indicator,"mousedown",this),h.removeEvent(t,"touchmove",this),h.removeEvent(t,"MSPointerMove",this),h.removeEvent(t,"mousemove",this),h.removeEvent(t,"touchend",this),h.removeEvent(t,"MSPointerUp",this),h.removeEvent(t,"mouseup",this)),this.options.defaultScrollbars&&this.wrapper.parentNode.removeChild(this.wrapper)},_start:function(i){var s=i.touches?i.touches[0]:i;i.preventDefault(),i.stopPropagation(),this.transitionTime(0),this.initiated=!0,this.moved=!1,this.lastPointX=s.pageX,this.lastPointY=s.pageY,this.startTime=h.getTime(),h.addEvent(t,"touchmove",this),h.addEvent(t,"MSPointerMove",this),h.addEvent(t,"mousemove",this),this.scroller._execEvent("scrollStart")},_move:function(t){var i,s,e,o,n=t.touches?t.touches[0]:t;h.getTime(),this.moved=!0,i=n.pageX-this.lastPointX,this.lastPointX=n.pageX,s=n.pageY-this.lastPointY,this.lastPointY=n.pageY,e=this.x+i,o=this.y+s,this._pos(e,o),t.preventDefault(),t.stopPropagation()},_end:function(i){if(this.initiated){if(this.initiated=!1,i.preventDefault(),i.stopPropagation(),h.removeEvent(t,"touchmove",this),h.removeEvent(t,"MSPointerMove",this),h.removeEvent(t,"mousemove",this),this.scroller.options.snap){var e=this.scroller._nearestSnap(this.scroller.x,this.scroller.y),o=this.options.snapSpeed||s.max(s.max(s.min(s.abs(this.scroller.x-e.x),1e3),s.min(s.abs(this.scroller.y-e.y),1e3)),300);(this.scroller.x!=e.x||this.scroller.y!=e.y)&&(this.scroller.directionX=0,this.scroller.directionY=0,this.scroller.currentPage=e,this.scroller.scrollTo(e.x,e.y,o,this.scroller.options.bounceEasing))}this.moved&&this.scroller._execEvent("scrollEnd")}},transitionTime:function(t){t=t||0,this.indicatorStyle[h.style.transitionDuration]=t+"ms"},transitionTimingFunction:function(t){this.indicatorStyle[h.style.transitionTimingFunction]=t},refresh:function(){this.transitionTime(0),this.indicatorStyle.display=this.options.listenX&&!this.options.listenY?this.scroller.hasHorizontalScroll?"block":"none":this.options.listenY&&!this.options.listenX?this.scroller.hasVerticalScroll?"block":"none":this.scroller.hasHorizontalScroll||this.scroller.hasVerticalScroll?"block":"none",this.scroller.hasHorizontalScroll&&this.scroller.hasVerticalScroll?(h.addClass(this.wrapper,"iScrollBothScrollbars"),h.removeClass(this.wrapper,"iScrollLoneScrollbar"),this.options.defaultScrollbars&&this.options.customStyle&&(this.options.listenX?this.wrapper.style.right="8px":this.wrapper.style.bottom="8px")):(h.removeClass(this.wrapper,"iScrollBothScrollbars"),h.addClass(this.wrapper,"iScrollLoneScrollbar"),this.options.defaultScrollbars&&this.options.customStyle&&(this.options.listenX?this.wrapper.style.right="2px":this.wrapper.style.bottom="2px")),this.wrapper.offsetHeight,this.options.listenX&&(this.wrapperWidth=this.wrapper.clientWidth,this.options.resize?(this.indicatorWidth=s.max(s.round(this.wrapperWidth*this.wrapperWidth/(this.scroller.scrollerWidth||this.wrapperWidth||1)),8),this.indicatorStyle.width=this.indicatorWidth+"px"):this.indicatorWidth=this.indicator.clientWidth,this.maxPosX=this.wrapperWidth-this.indicatorWidth,this.sizeRatioX=this.options.speedRatioX||this.scroller.maxScrollX&&this.maxPosX/this.scroller.maxScrollX),this.options.listenY&&(this.wrapperHeight=this.wrapper.clientHeight,this.options.resize?(this.indicatorHeight=s.max(s.round(this.wrapperHeight*this.wrapperHeight/(this.scroller.scrollerHeight||this.wrapperHeight||1)),8),this.indicatorStyle.height=this.indicatorHeight+"px"):this.indicatorHeight=this.indicator.clientHeight,this.maxPosY=this.wrapperHeight-this.indicatorHeight,this.sizeRatioY=this.options.speedRatioY||this.scroller.maxScrollY&&this.maxPosY/this.scroller.maxScrollY),this.updatePosition()},updatePosition:function(){var t=s.round(this.sizeRatioX*this.scroller.x)||0,i=s.round(this.sizeRatioY*this.scroller.y)||0;this.options.ignoreBoundaries||(0>t?t=0:t>this.maxPosX&&(t=this.maxPosX),0>i?i=0:i>this.maxPosY&&(i=this.maxPosY)),this.x=t,this.y=i,this.scroller.options.useTransform?this.indicatorStyle[h.style.transform]="translate("+t+"px,"+i+"px)"+this.scroller.translateZ:(this.indicatorStyle.left=t+"px",this.indicatorStyle.top=i+"px")},_pos:function(t,i){0>t?t=0:t>this.maxPosX&&(t=this.maxPosX),0>i?i=0:i>this.maxPosY&&(i=this.maxPosY),t=this.options.listenX?s.round(t/this.sizeRatioX):this.scroller.x,i=this.options.listenY?s.round(i/this.sizeRatioY):this.scroller.y,this.scroller.scrollTo(t,i)}},e.ease=h.ease,e}(window,document,Math); \ No newline at end of file +/*! iScroll v5.1.1 ~ (c) 2008-2014 Matteo Spinelli ~ http://cubiq.org/license */ +!function(t,i,s){function e(t,s){this.wrapper="string"==typeof t?i.querySelector(t):t,this.scroller=this.wrapper.children[0],this.scrollerStyle=this.scroller.style,this.options={resizeScrollbars:!0,mouseWheelSpeed:20,snapThreshold:.334,startX:0,startY:0,scrollY:!0,directionLockThreshold:5,momentum:!0,bounce:!0,bounceTime:600,bounceEasing:"",preventDefault:!0,preventDefaultException:{tagName:/^(INPUT|TEXTAREA|BUTTON|SELECT)$/},HWCompositing:!0,useTransition:!0,useTransform:!0};for(var e in s)this.options[e]=s[e];this.translateZ=this.options.HWCompositing&&h.hasPerspective?" translateZ(0)":"",this.options.useTransition=h.hasTransition&&this.options.useTransition,this.options.useTransform=h.hasTransform&&this.options.useTransform,this.options.eventPassthrough=this.options.eventPassthrough===!0?"vertical":this.options.eventPassthrough,this.options.preventDefault=!this.options.eventPassthrough&&this.options.preventDefault,this.options.scrollY="vertical"==this.options.eventPassthrough?!1:this.options.scrollY,this.options.scrollX="horizontal"==this.options.eventPassthrough?!1:this.options.scrollX,this.options.freeScroll=this.options.freeScroll&&!this.options.eventPassthrough,this.options.directionLockThreshold=this.options.eventPassthrough?0:this.options.directionLockThreshold,this.options.bounceEasing="string"==typeof this.options.bounceEasing?h.ease[this.options.bounceEasing]||h.ease.circular:this.options.bounceEasing,this.options.resizePolling=void 0===this.options.resizePolling?60:this.options.resizePolling,this.options.tap===!0&&(this.options.tap="tap"),"scale"==this.options.shrinkScrollbars&&(this.options.useTransition=!1),this.options.invertWheelDirection=this.options.invertWheelDirection?-1:1,this.x=0,this.y=0,this.directionX=0,this.directionY=0,this._events={},this._init(),this.refresh(),this.scrollTo(this.options.startX,this.options.startY),this.enable()}function o(t,s,e){var o=i.createElement("div"),n=i.createElement("div");return e===!0&&(o.style.cssText="position:absolute;z-index:9999",n.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px"),n.className="iScrollIndicator","h"==t?(e===!0&&(o.style.cssText+=";height:7px;left:2px;right:2px;bottom:0",n.style.height="100%"),o.className="iScrollHorizontalScrollbar"):(e===!0&&(o.style.cssText+=";width:7px;bottom:2px;top:2px;right:1px",n.style.width="100%"),o.className="iScrollVerticalScrollbar"),o.style.cssText+=";overflow:hidden",s||(o.style.pointerEvents="none"),o.appendChild(n),o}function n(s,e){this.wrapper="string"==typeof e.el?i.querySelector(e.el):e.el,this.wrapperStyle=this.wrapper.style,this.indicator=this.wrapper.children[0],this.indicatorStyle=this.indicator.style,this.scroller=s,this.options={listenX:!0,listenY:!0,interactive:!1,resize:!0,defaultScrollbars:!1,shrink:!1,fade:!1,speedRatioX:0,speedRatioY:0};for(var o in e)this.options[o]=e[o];this.sizeRatioX=1,this.sizeRatioY=1,this.maxPosX=0,this.maxPosY=0,this.options.interactive&&(this.options.disableTouch||(h.addEvent(this.indicator,"touchstart",this),h.addEvent(t,"touchend",this)),this.options.disablePointer||(h.addEvent(this.indicator,"MSPointerDown",this),h.addEvent(t,"MSPointerUp",this)),this.options.disableMouse||(h.addEvent(this.indicator,"mousedown",this),h.addEvent(t,"mouseup",this))),this.options.fade&&(this.wrapperStyle[h.style.transform]=this.scroller.translateZ,this.wrapperStyle[h.style.transitionDuration]=h.isBadAndroid?"0.001s":"0ms",this.wrapperStyle.opacity="0")}var r=t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||t.msRequestAnimationFrame||function(i){t.setTimeout(i,1e3/60)},h=function(){function e(t){return r===!1?!1:""===r?t:r+t.charAt(0).toUpperCase()+t.substr(1)}var o={},n=i.createElement("div").style,r=function(){for(var t,i=["t","webkitT","MozT","msT","OT"],s=0,e=i.length;e>s;s++)if(t=i[s]+"ransform",t in n)return i[s].substr(0,i[s].length-1);return!1}();o.getTime=Date.now||function(){return(new Date).getTime()},o.extend=function(t,i){for(var s in i)t[s]=i[s]},o.addEvent=function(t,i,s,e){t.addEventListener(i,s,!!e)},o.removeEvent=function(t,i,s,e){t.removeEventListener(i,s,!!e)},o.momentum=function(t,i,e,o,n,r){var h,a,l=t-i,c=s.abs(l)/e;return r=void 0===r?6e-4:r,h=t+c*c/(2*r)*(0>l?-1:1),a=c/r,o>h?(h=n?o-n/2.5*(c/8):o,l=s.abs(h-t),a=l/c):h>0&&(h=n?n/2.5*(c/8):0,l=s.abs(t)+h,a=l/c),{destination:s.round(h),duration:a}};var h=e("transform");return o.extend(o,{hasTransform:h!==!1,hasPerspective:e("perspective")in n,hasTouch:"ontouchstart"in t,hasPointer:navigator.msPointerEnabled,hasTransition:e("transition")in n}),o.isBadAndroid=/Android /.test(t.navigator.appVersion)&&!/Chrome\/\d/.test(t.navigator.appVersion),o.extend(o.style={},{transform:h,transitionTimingFunction:e("transitionTimingFunction"),transitionDuration:e("transitionDuration"),transitionDelay:e("transitionDelay"),transformOrigin:e("transformOrigin")}),o.hasClass=function(t,i){var s=new RegExp("(^|\\s)"+i+"(\\s|$)");return s.test(t.className)},o.addClass=function(t,i){if(!o.hasClass(t,i)){var s=t.className.split(" ");s.push(i),t.className=s.join(" ")}},o.removeClass=function(t,i){if(o.hasClass(t,i)){var s=new RegExp("(^|\\s)"+i+"(\\s|$)","g");t.className=t.className.replace(s," ")}},o.offset=function(t){for(var i=-t.offsetLeft,s=-t.offsetTop;t=t.offsetParent;)i-=t.offsetLeft,s-=t.offsetTop;return{left:i,top:s}},o.preventDefaultException=function(t,i){for(var s in i)if(i[s].test(t[s]))return!0;return!1},o.extend(o.eventType={},{touchstart:1,touchmove:1,touchend:1,mousedown:2,mousemove:2,mouseup:2,MSPointerDown:3,MSPointerMove:3,MSPointerUp:3}),o.extend(o.ease={},{quadratic:{style:"cubic-bezier(0.25, 0.46, 0.45, 0.94)",fn:function(t){return t*(2-t)}},circular:{style:"cubic-bezier(0.1, 0.57, 0.1, 1)",fn:function(t){return s.sqrt(1- --t*t)}},back:{style:"cubic-bezier(0.175, 0.885, 0.32, 1.275)",fn:function(t){var i=4;return(t-=1)*t*((i+1)*t+i)+1}},bounce:{style:"",fn:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?7.5625*(t-=1.5/2.75)*t+.75:2.5/2.75>t?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375}},elastic:{style:"",fn:function(t){var i=.22,e=.4;return 0===t?0:1==t?1:e*s.pow(2,-10*t)*s.sin(2*(t-i/4)*s.PI/i)+1}}}),o.tap=function(t,s){var e=i.createEvent("Event");e.initEvent(s,!0,!0),e.pageX=t.pageX,e.pageY=t.pageY,t.target.dispatchEvent(e)},o.click=function(t){var s,e=t.target;/(SELECT|INPUT|TEXTAREA)/i.test(e.tagName)||(s=i.createEvent("MouseEvents"),s.initMouseEvent("click",!0,!0,t.view,1,e.screenX,e.screenY,e.clientX,e.clientY,t.ctrlKey,t.altKey,t.shiftKey,t.metaKey,0,null),s._constructed=!0,e.dispatchEvent(s))},o}();e.prototype={version:"5.1.1",_init:function(){this._initEvents(),(this.options.scrollbars||this.options.indicators)&&this._initIndicators(),this.options.mouseWheel&&this._initWheel(),this.options.snap&&this._initSnap(),this.options.keyBindings&&this._initKeys()},destroy:function(){this._initEvents(!0),this._execEvent("destroy")},_transitionEnd:function(t){t.target==this.scroller&&this.isInTransition&&(this._transitionTime(),this.resetPosition(this.options.bounceTime)||(this.isInTransition=!1,this._execEvent("scrollEnd")))},_start:function(t){if(!(1!=h.eventType[t.type]&&0!==t.button||!this.enabled||this.initiated&&h.eventType[t.type]!==this.initiated)){!this.options.preventDefault||h.isBadAndroid||h.preventDefaultException(t.target,this.options.preventDefaultException)||t.preventDefault();var i,e=t.touches?t.touches[0]:t;this.initiated=h.eventType[t.type],this.moved=!1,this.distX=0,this.distY=0,this.directionX=0,this.directionY=0,this.directionLocked=0,this._transitionTime(),this.startTime=h.getTime(),this.options.useTransition&&this.isInTransition?(this.isInTransition=!1,i=this.getComputedPosition(),this._translate(s.round(i.x),s.round(i.y)),this._execEvent("scrollEnd")):!this.options.useTransition&&this.isAnimating&&(this.isAnimating=!1,this._execEvent("scrollEnd")),this.startX=this.x,this.startY=this.y,this.absStartX=this.x,this.absStartY=this.y,this.pointX=e.pageX,this.pointY=e.pageY,this._execEvent("beforeScrollStart")}},_move:function(t){if(this.enabled&&h.eventType[t.type]===this.initiated){this.options.preventDefault&&t.preventDefault();var i,e,o,n,r=t.touches?t.touches[0]:t,a=r.pageX-this.pointX,l=r.pageY-this.pointY,c=h.getTime();if(this.pointX=r.pageX,this.pointY=r.pageY,this.distX+=a,this.distY+=l,o=s.abs(this.distX),n=s.abs(this.distY),!(c-this.endTime>300&&10>o&&10>n)){if(this.directionLocked||this.options.freeScroll||(this.directionLocked=o>n+this.options.directionLockThreshold?"h":n>=o+this.options.directionLockThreshold?"v":"n"),"h"==this.directionLocked){if("vertical"==this.options.eventPassthrough)t.preventDefault();else if("horizontal"==this.options.eventPassthrough)return void(this.initiated=!1);l=0}else if("v"==this.directionLocked){if("horizontal"==this.options.eventPassthrough)t.preventDefault();else if("vertical"==this.options.eventPassthrough)return void(this.initiated=!1);a=0}a=this.hasHorizontalScroll?a:0,l=this.hasVerticalScroll?l:0,i=this.x+a,e=this.y+l,(i>0||i0?0:this.maxScrollX),(e>0||e0?0:this.maxScrollY),this.directionX=a>0?-1:0>a?1:0,this.directionY=l>0?-1:0>l?1:0,this.moved||this._execEvent("scrollStart"),this.moved=!0,this._translate(i,e),c-this.startTime>300&&(this.startTime=c,this.startX=this.x,this.startY=this.y)}}},_end:function(t){if(this.enabled&&h.eventType[t.type]===this.initiated){this.options.preventDefault&&!h.preventDefaultException(t.target,this.options.preventDefaultException)&&t.preventDefault();var i,e,o=(t.changedTouches?t.changedTouches[0]:t,h.getTime()-this.startTime),n=s.round(this.x),r=s.round(this.y),a=s.abs(n-this.startX),l=s.abs(r-this.startY),c=0,p="";if(this.isInTransition=0,this.initiated=0,this.endTime=h.getTime(),!this.resetPosition(this.options.bounceTime)){if(this.scrollTo(n,r),!this.moved)return this.options.tap&&h.tap(t,this.options.tap),this.options.click&&h.click(t),void this._execEvent("scrollCancel");if(this._events.flick&&200>o&&100>a&&100>l)return void this._execEvent("flick");if(this.options.momentum&&300>o&&(i=this.hasHorizontalScroll?h.momentum(this.x,this.startX,o,this.maxScrollX,this.options.bounce?this.wrapperWidth:0,this.options.deceleration):{destination:n,duration:0},e=this.hasVerticalScroll?h.momentum(this.y,this.startY,o,this.maxScrollY,this.options.bounce?this.wrapperHeight:0,this.options.deceleration):{destination:r,duration:0},n=i.destination,r=e.destination,c=s.max(i.duration,e.duration),this.isInTransition=1),this.options.snap){var d=this._nearestSnap(n,r);this.currentPage=d,c=this.options.snapSpeed||s.max(s.max(s.min(s.abs(n-d.x),1e3),s.min(s.abs(r-d.y),1e3)),300),n=d.x,r=d.y,this.directionX=0,this.directionY=0,p=this.options.bounceEasing}return n!=this.x||r!=this.y?((n>0||n0||r0?i=0:this.x0?s=0:this.y-1&&this._events[t].splice(s,1)}},_execEvent:function(t){if(this._events[t]){var i=0,s=this._events[t].length;if(s)for(;s>i;i++)this._events[t][i].apply(this,[].slice.call(arguments,1))}},scrollBy:function(t,i,s,e){t=this.x+t,i=this.y+i,s=s||0,this.scrollTo(t,i,s,e)},scrollTo:function(t,i,s,e){e=e||h.ease.circular,this.isInTransition=this.options.useTransition&&s>0,!s||this.options.useTransition&&e.style?(this._transitionTimingFunction(e.style),this._transitionTime(s),this._translate(t,i)):this._animate(t,i,s,e.fn)},scrollToElement:function(t,i,e,o,n){if(t=t.nodeType?t:this.scroller.querySelector(t)){var r=h.offset(t);r.left-=this.wrapperOffset.left,r.top-=this.wrapperOffset.top,e===!0&&(e=s.round(t.offsetWidth/2-this.wrapper.offsetWidth/2)),o===!0&&(o=s.round(t.offsetHeight/2-this.wrapper.offsetHeight/2)),r.left-=e||0,r.top-=o||0,r.left=r.left>0?0:r.left0?0:r.top0?o--:0>i&&o++,e>0?n--:0>e&&n++,void this.goToPage(o,n);o=this.x+s.round(this.hasHorizontalScroll?i:0),n=this.y+s.round(this.hasVerticalScroll?e:0),o>0?o=0:o0?n=0:n-this.scrollerWidth;){for(this.pages[h]=[],t=0,n=0;n>-this.scrollerHeight;)this.pages[h][t]={x:s.max(l,this.maxScrollX),y:s.max(n,this.maxScrollY),width:c,height:p,cx:l-e,cy:n-o},n-=p,t++;l-=c,h++}else for(r=this.options.snap,t=r.length,i=-1;t>h;h++)(0===h||r[h].offsetLeft<=r[h-1].offsetLeft)&&(a=0,i++),this.pages[a]||(this.pages[a]=[]),l=s.max(-r[h].offsetLeft,this.maxScrollX),n=s.max(-r[h].offsetTop,this.maxScrollY),e=l-s.round(r[h].offsetWidth/2),o=n-s.round(r[h].offsetHeight/2),this.pages[a][i]={x:l,y:n,width:r[h].offsetWidth,height:r[h].offsetHeight,cx:e,cy:o},l>this.maxScrollX&&a++;this.goToPage(this.currentPage.pageX||0,this.currentPage.pageY||0,0),this.options.snapThreshold%1===0?(this.snapThresholdX=this.options.snapThreshold,this.snapThresholdY=this.options.snapThreshold):(this.snapThresholdX=s.round(this.pages[this.currentPage.pageX][this.currentPage.pageY].width*this.options.snapThreshold),this.snapThresholdY=s.round(this.pages[this.currentPage.pageX][this.currentPage.pageY].height*this.options.snapThreshold))}}),this.on("flick",function(){var t=this.options.snapSpeed||s.max(s.max(s.min(s.abs(this.x-this.startX),1e3),s.min(s.abs(this.y-this.startY),1e3)),300);this.goToPage(this.currentPage.pageX+this.directionX,this.currentPage.pageY+this.directionY,t)})},_nearestSnap:function(t,i){if(!this.pages.length)return{x:0,y:0,pageX:0,pageY:0};var e=0,o=this.pages.length,n=0;if(s.abs(t-this.absStartX)0?t=0:t0?i=0:ie;e++)if(t>=this.pages[e][0].cx){t=this.pages[e][0].x;break}for(o=this.pages[e].length;o>n;n++)if(i>=this.pages[0][n].cy){i=this.pages[0][n].y;break}return e==this.currentPage.pageX&&(e+=this.directionX,0>e?e=0:e>=this.pages.length&&(e=this.pages.length-1),t=this.pages[e][0].x),n==this.currentPage.pageY&&(n+=this.directionY,0>n?n=0:n>=this.pages[0].length&&(n=this.pages[0].length-1),i=this.pages[0][n].y),{x:t,y:i,pageX:e,pageY:n}},goToPage:function(t,i,e,o){o=o||this.options.bounceEasing,t>=this.pages.length?t=this.pages.length-1:0>t&&(t=0),i>=this.pages[t].length?i=this.pages[t].length-1:0>i&&(i=0);var n=this.pages[t][i].x,r=this.pages[t][i].y;e=void 0===e?this.options.snapSpeed||s.max(s.max(s.min(s.abs(n-this.x),1e3),s.min(s.abs(r-this.y),1e3)),300):e,this.currentPage={x:n,y:r,pageX:t,pageY:i},this.scrollTo(n,r,e,o)},next:function(t,i){var s=this.currentPage.pageX,e=this.currentPage.pageY;s++,s>=this.pages.length&&this.hasVerticalScroll&&(s=0,e++),this.goToPage(s,e,t,i)},prev:function(t,i){var s=this.currentPage.pageX,e=this.currentPage.pageY;s--,0>s&&this.hasVerticalScroll&&(s=0,e--),this.goToPage(s,e,t,i)},_initKeys:function(){var i,s={pageUp:33,pageDown:34,end:35,home:36,left:37,up:38,right:39,down:40};if("object"==typeof this.options.keyBindings)for(i in this.options.keyBindings)"string"==typeof this.options.keyBindings[i]&&(this.options.keyBindings[i]=this.options.keyBindings[i].toUpperCase().charCodeAt(0));else this.options.keyBindings={};for(i in s)this.options.keyBindings[i]=this.options.keyBindings[i]||s[i];h.addEvent(t,"keydown",this),this.on("destroy",function(){h.removeEvent(t,"keydown",this)})},_key:function(t){if(this.enabled){var i,e=this.options.snap,o=e?this.currentPage.pageX:this.x,n=e?this.currentPage.pageY:this.y,r=h.getTime(),a=this.keyTime||0,l=.25;switch(this.options.useTransition&&this.isInTransition&&(i=this.getComputedPosition(),this._translate(s.round(i.x),s.round(i.y)),this.isInTransition=!1),this.keyAcceleration=200>r-a?s.min(this.keyAcceleration+l,50):0,t.keyCode){case this.options.keyBindings.pageUp:this.hasHorizontalScroll&&!this.hasVerticalScroll?o+=e?1:this.wrapperWidth:n+=e?1:this.wrapperHeight;break;case this.options.keyBindings.pageDown:this.hasHorizontalScroll&&!this.hasVerticalScroll?o-=e?1:this.wrapperWidth:n-=e?1:this.wrapperHeight;break;case this.options.keyBindings.end:o=e?this.pages.length-1:this.maxScrollX,n=e?this.pages[0].length-1:this.maxScrollY;break;case this.options.keyBindings.home:o=0,n=0;break;case this.options.keyBindings.left:o+=e?-1:5+this.keyAcceleration>>0;break;case this.options.keyBindings.up:n+=e?1:5+this.keyAcceleration>>0;break;case this.options.keyBindings.right:o-=e?-1:5+this.keyAcceleration>>0;break;case this.options.keyBindings.down:n-=e?1:5+this.keyAcceleration>>0;break;default:return}if(e)return void this.goToPage(o,n);o>0?(o=0,this.keyAcceleration=0):o0?(n=0,this.keyAcceleration=0):n=p?(n.isAnimating=!1,n._translate(t,i),void(n.resetPosition(n.options.bounceTime)||n._execEvent("scrollEnd"))):(f=(f-c)/s,m=e(f),d=(t-a)*m+a,u=(i-l)*m+l,n._translate(d,u),void(n.isAnimating&&r(o)))}var n=this,a=this.x,l=this.y,c=h.getTime(),p=c+s;this.isAnimating=!0,o()},handleEvent:function(t){switch(t.type){case"touchstart":case"MSPointerDown":case"mousedown":this._start(t);break;case"touchmove":case"MSPointerMove":case"mousemove":this._move(t);break;case"touchend":case"MSPointerUp":case"mouseup":case"touchcancel":case"MSPointerCancel":case"mousecancel":this._end(t);break;case"orientationchange":case"resize":this._resize();break;case"transitionend":case"webkitTransitionEnd":case"oTransitionEnd":case"MSTransitionEnd":this._transitionEnd(t);break;case"wheel":case"DOMMouseScroll":case"mousewheel":this._wheel(t);break;case"keydown":this._key(t);break;case"click":t._constructed||(t.preventDefault(),t.stopPropagation())}}},n.prototype={handleEvent:function(t){switch(t.type){case"touchstart":case"MSPointerDown":case"mousedown":this._start(t);break;case"touchmove":case"MSPointerMove":case"mousemove":this._move(t);break;case"touchend":case"MSPointerUp":case"mouseup":case"touchcancel":case"MSPointerCancel":case"mousecancel":this._end(t)}},destroy:function(){this.options.interactive&&(h.removeEvent(this.indicator,"touchstart",this),h.removeEvent(this.indicator,"MSPointerDown",this),h.removeEvent(this.indicator,"mousedown",this),h.removeEvent(t,"touchmove",this),h.removeEvent(t,"MSPointerMove",this),h.removeEvent(t,"mousemove",this),h.removeEvent(t,"touchend",this),h.removeEvent(t,"MSPointerUp",this),h.removeEvent(t,"mouseup",this)),this.options.defaultScrollbars&&this.wrapper.parentNode.removeChild(this.wrapper)},_start:function(i){var s=i.touches?i.touches[0]:i;i.preventDefault(),i.stopPropagation(),this.transitionTime(),this.initiated=!0,this.moved=!1,this.lastPointX=s.pageX,this.lastPointY=s.pageY,this.startTime=h.getTime(),this.options.disableTouch||h.addEvent(t,"touchmove",this),this.options.disablePointer||h.addEvent(t,"MSPointerMove",this),this.options.disableMouse||h.addEvent(t,"mousemove",this),this.scroller._execEvent("beforeScrollStart")},_move:function(t){{var i,s,e,o,n=t.touches?t.touches[0]:t;h.getTime()}this.moved||this.scroller._execEvent("scrollStart"),this.moved=!0,i=n.pageX-this.lastPointX,this.lastPointX=n.pageX,s=n.pageY-this.lastPointY,this.lastPointY=n.pageY,e=this.x+i,o=this.y+s,this._pos(e,o),t.preventDefault(),t.stopPropagation()},_end:function(i){if(this.initiated){if(this.initiated=!1,i.preventDefault(),i.stopPropagation(),h.removeEvent(t,"touchmove",this),h.removeEvent(t,"MSPointerMove",this),h.removeEvent(t,"mousemove",this),this.scroller.options.snap){var e=this.scroller._nearestSnap(this.scroller.x,this.scroller.y),o=this.options.snapSpeed||s.max(s.max(s.min(s.abs(this.scroller.x-e.x),1e3),s.min(s.abs(this.scroller.y-e.y),1e3)),300);(this.scroller.x!=e.x||this.scroller.y!=e.y)&&(this.scroller.directionX=0,this.scroller.directionY=0,this.scroller.currentPage=e,this.scroller.scrollTo(e.x,e.y,o,this.scroller.options.bounceEasing))}this.moved&&this.scroller._execEvent("scrollEnd")}},transitionTime:function(t){t=t||0,this.indicatorStyle[h.style.transitionDuration]=t+"ms",!t&&h.isBadAndroid&&(this.indicatorStyle[h.style.transitionDuration]="0.001s")},transitionTimingFunction:function(t){this.indicatorStyle[h.style.transitionTimingFunction]=t},refresh:function(){this.transitionTime(),this.indicatorStyle.display=this.options.listenX&&!this.options.listenY?this.scroller.hasHorizontalScroll?"block":"none":this.options.listenY&&!this.options.listenX?this.scroller.hasVerticalScroll?"block":"none":this.scroller.hasHorizontalScroll||this.scroller.hasVerticalScroll?"block":"none",this.scroller.hasHorizontalScroll&&this.scroller.hasVerticalScroll?(h.addClass(this.wrapper,"iScrollBothScrollbars"),h.removeClass(this.wrapper,"iScrollLoneScrollbar"),this.options.defaultScrollbars&&this.options.customStyle&&(this.options.listenX?this.wrapper.style.right="8px":this.wrapper.style.bottom="8px")):(h.removeClass(this.wrapper,"iScrollBothScrollbars"),h.addClass(this.wrapper,"iScrollLoneScrollbar"),this.options.defaultScrollbars&&this.options.customStyle&&(this.options.listenX?this.wrapper.style.right="2px":this.wrapper.style.bottom="2px"));this.wrapper.offsetHeight;this.options.listenX&&(this.wrapperWidth=this.wrapper.clientWidth,this.options.resize?(this.indicatorWidth=s.max(s.round(this.wrapperWidth*this.wrapperWidth/(this.scroller.scrollerWidth||this.wrapperWidth||1)),8),this.indicatorStyle.width=this.indicatorWidth+"px"):this.indicatorWidth=this.indicator.clientWidth,this.maxPosX=this.wrapperWidth-this.indicatorWidth,"clip"==this.options.shrink?(this.minBoundaryX=-this.indicatorWidth+8,this.maxBoundaryX=this.wrapperWidth-8):(this.minBoundaryX=0,this.maxBoundaryX=this.maxPosX),this.sizeRatioX=this.options.speedRatioX||this.scroller.maxScrollX&&this.maxPosX/this.scroller.maxScrollX),this.options.listenY&&(this.wrapperHeight=this.wrapper.clientHeight,this.options.resize?(this.indicatorHeight=s.max(s.round(this.wrapperHeight*this.wrapperHeight/(this.scroller.scrollerHeight||this.wrapperHeight||1)),8),this.indicatorStyle.height=this.indicatorHeight+"px"):this.indicatorHeight=this.indicator.clientHeight,this.maxPosY=this.wrapperHeight-this.indicatorHeight,"clip"==this.options.shrink?(this.minBoundaryY=-this.indicatorHeight+8,this.maxBoundaryY=this.wrapperHeight-8):(this.minBoundaryY=0,this.maxBoundaryY=this.maxPosY),this.maxPosY=this.wrapperHeight-this.indicatorHeight,this.sizeRatioY=this.options.speedRatioY||this.scroller.maxScrollY&&this.maxPosY/this.scroller.maxScrollY),this.updatePosition()},updatePosition:function(){var t=this.options.listenX&&s.round(this.sizeRatioX*this.scroller.x)||0,i=this.options.listenY&&s.round(this.sizeRatioY*this.scroller.y)||0;this.options.ignoreBoundaries||(tthis.maxBoundaryX?"scale"==this.options.shrink?(this.width=s.max(this.indicatorWidth-(t-this.maxPosX),8),this.indicatorStyle.width=this.width+"px",t=this.maxPosX+this.indicatorWidth-this.width):t=this.maxBoundaryX:"scale"==this.options.shrink&&this.width!=this.indicatorWidth&&(this.width=this.indicatorWidth,this.indicatorStyle.width=this.width+"px"),ithis.maxBoundaryY?"scale"==this.options.shrink?(this.height=s.max(this.indicatorHeight-3*(i-this.maxPosY),8),this.indicatorStyle.height=this.height+"px",i=this.maxPosY+this.indicatorHeight-this.height):i=this.maxBoundaryY:"scale"==this.options.shrink&&this.height!=this.indicatorHeight&&(this.height=this.indicatorHeight,this.indicatorStyle.height=this.height+"px")),this.x=t,this.y=i,this.scroller.options.useTransform?this.indicatorStyle[h.style.transform]="translate("+t+"px,"+i+"px)"+this.scroller.translateZ:(this.indicatorStyle.left=t+"px",this.indicatorStyle.top=i+"px")},_pos:function(t,i){0>t?t=0:t>this.maxPosX&&(t=this.maxPosX),0>i?i=0:i>this.maxPosY&&(i=this.maxPosY),t=this.options.listenX?s.round(t/this.sizeRatioX):this.scroller.x,i=this.options.listenY?s.round(i/this.sizeRatioY):this.scroller.y,this.scroller.scrollTo(t,i)},fade:function(t,i){if(!i||this.visible){clearTimeout(this.fadeTimeout),this.fadeTimeout=null;var s=t?250:500,e=t?0:300;t=t?"1":"0",this.wrapperStyle[h.style.transitionDuration]=s+"ms",this.fadeTimeout=setTimeout(function(t){this.wrapperStyle.opacity=t,this.visible=+t}.bind(this,t),e)}}},e.utils=h,"undefined"!=typeof module&&module.exports?module.exports=e:t.IScroll=e}(window,document,Math); \ No newline at end of file diff --git a/src/tpl/mail-list.html b/src/tpl/mail-list.html index 473aee1..105b15e 100644 --- a/src/tpl/mail-list.html +++ b/src/tpl/mail-list.html @@ -10,7 +10,7 @@
    -
  • +
  • {{email.from[0].name || email.from[0].address}}

    diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 7cf6ff5..3eb6927 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -929,19 +929,16 @@ define(function(require) { }); describe('getMessageContent', function() { - it('should not do anything if the message already has content', function(done) { + it('should not do anything if the message already has content', function() { var message = { body: 'bender is great!' }; dao.getMessageContent({ message: message - }, function(err, msg) { - expect(err).to.not.exist; - expect(msg).to.equal(message); - - done(); }); + + // should do nothing }); it('should read an unencrypted body from the device', function(done) { @@ -957,7 +954,7 @@ define(function(require) { localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder, uid: uid - }).yields(null, [{ + }).yieldsAsync(null, [{ body: body }]); @@ -967,13 +964,17 @@ define(function(require) { 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(msg.loadingBody).to.be.false; + expect(localListStub.calledOnce).to.be.true; done(); }); + expect(message.loadingBody).to.be.true; }); it('should read an encrypted body from the device', function(done) { @@ -989,7 +990,7 @@ define(function(require) { localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder, uid: uid - }).yields(null, [{ + }).yieldsAsync(null, [{ body: body }]); @@ -999,14 +1000,18 @@ define(function(require) { 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(message.loadingBody).to.be.false; + expect(localListStub.calledOnce).to.be.true; done(); }); + expect(message.loadingBody).to.be.true; }); it('should stream an unencrypted body from imap', function(done) { @@ -1022,12 +1027,12 @@ define(function(require) { localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder, uid: uid - }).yields(null, [{}]); + }).yieldsAsync(null, [{}]); localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ folder: folder, emails: [message] - }).yields(); + }).yieldsAsync(); imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { expect(opts).to.deep.equal({ @@ -1045,15 +1050,19 @@ define(function(require) { 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(msg.loadingBody).to.be.false; + expect(localListStub.calledOnce).to.be.true; expect(imapStreamStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; done(); }); + expect(message.loadingBody).to.be.true; }); it('should stream an encrypted body from imap', function(done) { @@ -1069,12 +1078,12 @@ define(function(require) { localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder, uid: uid - }).yields(null, [{}]); + }).yieldsAsync(null, [{}]); localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({ folder: folder, emails: [message] - }).yields(); + }).yieldsAsync(); imapStreamStub = sinon.stub(dao, '_imapStreamText', function(opts, cb) { expect(opts).to.deep.equal({ @@ -1092,17 +1101,20 @@ define(function(require) { 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(msg.loadingBody).to.be.false; + expect(localListStub.calledOnce).to.be.true; expect(imapStreamStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; - done(); }); + expect(message.loadingBody).to.be.true; }); it('fail to stream from imap due to error when persisting', function(done) { @@ -1139,6 +1151,8 @@ define(function(require) { expect(imapStreamStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; + expect(message.loadingBody).to.be.false; + done(); }); }); @@ -1174,28 +1188,27 @@ define(function(require) { expect(imapStreamStub.calledOnce).to.be.true; expect(localStoreStub.called).to.be.false; + expect(message.loadingBody).to.be.false; + done(); }); }); }); describe('decryptMessageContent', function() { - it('should not do anything when the message is not encrypted', function(done) { + it('should not do anything when the message is not encrypted', function() { var message = { encrypted: false }; dao.decryptMessageContent({ message: message - }, function(error, msg) { - expect(error).to.not.exist; - expect(msg).to.equal(message); - - done(); }); + + // should do nothing }); - it('should not do anything when the message is already decrypted', function(done) { + it('should not do anything when the message is already decrypted', function() { var message = { encrypted: true, decrypted: true @@ -1203,12 +1216,9 @@ define(function(require) { dao.decryptMessageContent({ message: message - }, function(error, msg) { - expect(error).to.not.exist; - expect(msg).to.equal(message); - - done(); }); + + // should do nothing }); it('decrypt a pgp/mime message', function(done) { @@ -1224,8 +1234,8 @@ define(function(require) { 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); + 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.block).to.equal(mimeBody); @@ -1238,15 +1248,20 @@ define(function(require) { 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(msg.decryptingBody).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.calledOnce).to.be.true; done(); }); + + expect(message.decryptingBody).to.be.true; }); it('decrypt a pgp/inline message', function(done) { @@ -1261,23 +1276,27 @@ define(function(require) { 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); + 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.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(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) { @@ -1303,9 +1322,12 @@ define(function(require) { 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(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.called).to.be.false; @@ -1331,8 +1353,12 @@ define(function(require) { message: message }, function(error, msg) { expect(error).to.exist; + 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; diff --git a/test/new-unit/mail-list-ctrl-test.js b/test/new-unit/mail-list-ctrl-test.js index e5b1e49..0d96be7 100644 --- a/test/new-unit/mail-list-ctrl-test.js +++ b/test/new-unit/mail-list-ctrl-test.js @@ -10,6 +10,8 @@ define(function(require) { KeychainDAO = require('js/dao/keychain-dao'), appController = require('js/app-controller'); + chai.Assertion.includeStack = true; + describe('Mail List controller unit test', function() { var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, emailAddress, notificationClickedHandler, emails, @@ -223,6 +225,72 @@ define(function(require) { }); }); + describe('getContent', function() { + it('should get the mail content', function() { + scope.state.nav = { + currentFolder: { + type: 'asd', + } + }; + + scope.getContent(); + expect(emailDaoMock.getMessageContent.calledOnce).to.be.true; + }); + }); + + describe('select', function() { + it('should decrypt, focus mark an unread mail as read', function() { + var mail, synchronizeMock; + + mail = { + unread: true + }; + synchronizeMock = sinon.stub(scope, 'synchronize'); + scope.state = { + nav: { + currentFolder: { + type: 'asd', + } + }, + mailList: {}, + read: { + toggle: function() {} + } + }; + + scope.select(mail); + + expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true; + expect(synchronizeMock.calledOnce).to.be.true; + expect(scope.state.mailList.selected).to.equal(mail); + + scope.synchronize.restore(); + }); + + it('should decrypt and focus a read mail', function() { + var mail, synchronizeMock; + + mail = { + unread: false + }; + synchronizeMock = sinon.stub(scope, 'synchronize'); + scope.state = { + mailList: {}, + read: { + toggle: function() {} + } + }; + + scope.select(mail); + + expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true; + expect(synchronizeMock.called).to.be.false; + expect(scope.state.mailList.selected).to.equal(mail); + + scope.synchronize.restore(); + }); + }); + describe('remove', function() { it('should not delete without a selected mail', function() { scope.remove(); From f770b5256605d74dd21a53a1a9b9d223ba448e2a Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 18 Feb 2014 12:37:10 +0100 Subject: [PATCH 03/10] hide cyphertext from dom --- src/sass/views/_read.scss | 2 +- src/tpl/read.html | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/sass/views/_read.scss b/src/sass/views/_read.scss index 1e387fb..c3ab26d 100644 --- a/src/sass/views/_read.scss +++ b/src/sass/views/_read.scss @@ -85,7 +85,7 @@ height: 100%; overflow-y: scroll; - div { + &.line { word-wrap: break-word; &.empty-line { diff --git a/src/tpl/read.html b/src/tpl/read.html index 2f0184a..6155e88 100644 --- a/src/tpl/read.html +++ b/src/tpl/read.html @@ -36,10 +36,17 @@
    -
    - -
    - {{line}}
    +
    +
    +
    + +
    + {{line}}
    +
    +
    +
    + This message contains encrypted content. +
    From f8722a69320b0392b30177cf80d0da5b26f7855c Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 18 Feb 2014 13:15:42 +0100 Subject: [PATCH 04/10] change rule to not display ciphertext --- src/tpl/read.html | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/tpl/read.html b/src/tpl/read.html index 6155e88..9e8231a 100644 --- a/src/tpl/read.html +++ b/src/tpl/read.html @@ -36,16 +36,12 @@
    -
    +
    -
    +
    This message contains encrypted content.
    +
    -
    - {{line}}
    -
    -
    -
    - This message contains encrypted content. +
    {{line}}
    From 700ecca0a3e556cb11a98e6d57c3a9715bda927d Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 18 Feb 2014 13:32:40 +0100 Subject: [PATCH 05/10] fix css rules --- src/sass/views/_read.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sass/views/_read.scss b/src/sass/views/_read.scss index c3ab26d..6b11241 100644 --- a/src/sass/views/_read.scss +++ b/src/sass/views/_read.scss @@ -85,7 +85,7 @@ height: 100%; overflow-y: scroll; - &.line { + .line { word-wrap: break-word; &.empty-line { From 8973c3e2b3d0c20f7cf7f1f52102d15398b4d877 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 18 Feb 2014 17:05:51 +0100 Subject: [PATCH 06/10] exclude outbox from normal workflow --- src/js/controller/mail-list.js | 21 +++++++++++++++------ test/new-unit/mail-list-ctrl-test.js | 7 ++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 7704bdc..7ae66ee 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -59,6 +59,11 @@ define(function(require) { // $scope.getContent = function(email) { + // don't stream message content of outbox messages... + if (getFolder().type === 'Outbox') { + return; + } + emailDao.getMessageContent({ folder: getFolder().path, message: email @@ -72,17 +77,21 @@ define(function(require) { * Called when clicking on an email list item */ $scope.select = function(email) { + // unselect an item if (!email) { $scope.state.mailList.selected = undefined; return; } - emailDao.decryptMessageContent({ - message: email - }, function(error) { - $scope.$apply(); - $scope.onError(error); - }); + // if we're in the outbox, don't decrypt as usual + if (getFolder().type !== 'Outbox') { + emailDao.decryptMessageContent({ + message: email + }, function(error) { + $scope.$apply(); + $scope.onError(error); + }); + } $scope.state.mailList.selected = email; $scope.state.read.toggle(true); diff --git a/test/new-unit/mail-list-ctrl-test.js b/test/new-unit/mail-list-ctrl-test.js index 0d96be7..093f72e 100644 --- a/test/new-unit/mail-list-ctrl-test.js +++ b/test/new-unit/mail-list-ctrl-test.js @@ -249,7 +249,7 @@ define(function(require) { scope.state = { nav: { currentFolder: { - type: 'asd', + type: 'asd' } }, mailList: {}, @@ -278,6 +278,11 @@ define(function(require) { mailList: {}, read: { toggle: function() {} + }, + nav: { + currentFolder: { + type: 'asd' + } } }; From 6a8bb527fcc5d1fd44d92223c588ab7f9af0647a Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 20 Feb 2014 11:34:55 +0100 Subject: [PATCH 07/10] fix bug when uid smaller than max uid in memory exists on imap --- src/js/dao/email-dao.js | 12 ++++++------ test/new-unit/email-dao-test.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 04944d9..e17208f 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -437,12 +437,6 @@ define(function(require) { 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 @@ -456,6 +450,12 @@ define(function(require) { }); } + // no delta, we're done here + if (_.isEmpty(delta4)) { + doDeltaF4(); + return; + } + self._imapListMessages({ folder: folder.path, firstUid: Math.min.apply(null, delta4), diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 3eb6927..1c332bf 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -1476,7 +1476,7 @@ define(function(require) { imapSearchStub = sinon.stub(dao, '_imapSearch'); imapSearchStub.withArgs({ folder: folder - }).yields(null, [dummyEncryptedMail.uid]); + }).yields(null, [dummyEncryptedMail.uid - 10, dummyEncryptedMail.uid]); imapSearchStub.withArgs({ folder: folder, unread: true From b093b069f6a6a430f6161332f23b2286c7b42ef0 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 20 Feb 2014 15:42:51 +0100 Subject: [PATCH 08/10] review mail-list --- src/js/controller/mail-list.js | 55 ++++++++++++---------- src/js/dao/email-dao.js | 4 +- test/new-unit/email-dao-test.js | 70 ++++++++++++++++------------ test/new-unit/mail-list-ctrl-test.js | 6 +-- 4 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 7ae66ee..db67bdf 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -58,13 +58,13 @@ define(function(require) { // scope functions // - $scope.getContent = function(email) { + $scope.getBody = function(email) { // don't stream message content of outbox messages... if (getFolder().type === 'Outbox') { return; } - emailDao.getMessageContent({ + emailDao.getBody({ folder: getFolder().path, message: email }, function(error) { @@ -410,43 +410,48 @@ define(function(require) { ngModule.directive('ngIscroll', function() { return { link: function(scope, elm, attrs) { - var model = attrs.ngIscroll; + var model = attrs.ngIscroll, + listEl = elm[0]; + scope.$watch(model, function() { var myScroll; // activate iscroll - myScroll = new IScroll(elm[0], { - mouseWheel: true, + myScroll = new IScroll(listEl, { + mouseWheel: true }); // load the visible message bodies, when the list is re-initialized and when scrolling stopped loadVisible(); myScroll.on('scrollEnd', loadVisible); + }, true); - function loadVisible() { - var list = elm[0].getBoundingClientRect(), - footerHeight = elm[0].nextElementSibling.getBoundingClientRect().height, - top = list.top, - bottom = list.bottom - footerHeight, - listItems = elm[0].children[0].children, - i = listItems.length, - listItem, message, - isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible; + /* + * iterates over the mails in the mail list and loads their bodies if they are visible in the viewport + */ + function loadVisible() { + var listBorder = listEl.getBoundingClientRect(), + top = listBorder.top, + bottom = listBorder.bottom, + listItems = listEl.children[0].children, + i = listItems.length, + listItem, message, + isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible; - while (i--) { - listItem = listItems.item(i).getBoundingClientRect(); - message = scope.filteredMessages[i]; + while (i--) { + // the n-th list item (the dom representation of an email) corresponds to + // the n-th message model in the filteredMessages array + listItem = listItems.item(i).getBoundingClientRect(); + message = scope.filteredMessages[i]; - isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top - isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom - isVisible = listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole + isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top + isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom + isVisible = listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole - - if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) { - scope.getContent(message); - } + if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) { + scope.getBody(message); } } - }, true); + } } }; }); diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index e17208f..8998694 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -737,7 +737,7 @@ define(function(require) { * @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) { + EmailDAO.prototype.getBody = function(options, callback) { var self = this, message = options.message, folder = options.folder; @@ -1179,7 +1179,7 @@ define(function(require) { return; } - self._imapClient.streamPlaintext({ + self._imapClient.getBody({ 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 1c332bf..61cb7ed 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -821,7 +821,7 @@ define(function(require) { it('should work', function(done) { var path = 'FOLDAAAA'; - imapClientStub.streamPlaintext.withArgs({ + imapClientStub.getBody.withArgs({ path: path, message: {} }).yields(null, {}); @@ -833,16 +833,16 @@ define(function(require) { expect(err).to.not.exist; expect(msg).to.exist; - expect(imapClientStub.streamPlaintext.calledOnce).to.be.true; + expect(imapClientStub.getBody.calledOnce).to.be.true; done(); }); }); - it('should not work when streamPlaintext fails', function(done) { + it('should not work when getBody fails', function(done) { var path = 'FOLDAAAA'; - imapClientStub.streamPlaintext.yields({}); + imapClientStub.getBody.yields({}); dao._imapStreamText({ folder: path, @@ -851,7 +851,7 @@ define(function(require) { expect(err).to.exist; expect(msg).to.not.exist; - expect(imapClientStub.streamPlaintext.calledOnce).to.be.true; + expect(imapClientStub.getBody.calledOnce).to.be.true; done(); }); @@ -928,13 +928,13 @@ define(function(require) { }); }); - describe('getMessageContent', function() { + describe('getBody', function() { it('should not do anything if the message already has content', function() { var message = { body: 'bender is great!' }; - dao.getMessageContent({ + dao.getBody({ message: message }); @@ -959,12 +959,12 @@ define(function(require) { }]); - dao.getMessageContent({ + dao.getBody({ 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; @@ -995,18 +995,18 @@ define(function(require) { }]); - dao.getMessageContent({ + dao.getBody({ 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(message.loadingBody).to.be.false; - + expect(localListStub.calledOnce).to.be.true; done(); @@ -1045,7 +1045,7 @@ define(function(require) { }); - dao.getMessageContent({ + dao.getBody({ message: message, folder: folder }, function(err, msg) { @@ -1096,18 +1096,18 @@ define(function(require) { }); - dao.getMessageContent({ + dao.getBody({ 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(msg.loadingBody).to.be.false; - + expect(localListStub.calledOnce).to.be.true; expect(imapStreamStub.calledOnce).to.be.true; expect(localStoreStub.calledOnce).to.be.true; @@ -1141,7 +1141,7 @@ define(function(require) { emails: [message] }).yields({}); - dao.getMessageContent({ + dao.getBody({ message: message, folder: folder }, function(err, msg) { @@ -1178,7 +1178,7 @@ define(function(require) { localStoreStub = sinon.stub(dao, '_localStoreMessages'); - dao.getMessageContent({ + dao.getBody({ message: message, folder: folder }, function(err, msg) { @@ -1225,7 +1225,9 @@ define(function(require) { var message, parsedBody, mimeBody, parseStub; message = { - from: [{address: 'asdasdasd'}], + from: [{ + address: 'asdasdasd' + }], encrypted: true, decrypted: false, body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' @@ -1236,7 +1238,7 @@ define(function(require) { 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){ + parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb) { expect(o.message).to.equal(message); expect(o.block).to.equal(mimeBody); @@ -1248,12 +1250,12 @@ define(function(require) { 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(msg.decryptingBody).to.be.false; - + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.calledOnce).to.be.true; @@ -1268,7 +1270,9 @@ define(function(require) { var message, plaintextBody, parseStub; message = { - from: [{address: 'asdasdasd'}], + from: [{ + address: 'asdasdasd' + }], encrypted: true, decrypted: false, body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' @@ -1284,12 +1288,12 @@ define(function(require) { 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; @@ -1303,7 +1307,9 @@ define(function(require) { var message, plaintextBody, parseStub, errMsg; message = { - from: [{address: 'asdasdasd'}], + from: [{ + address: 'asdasdasd' + }], encrypted: true, decrypted: false, body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' @@ -1322,7 +1328,7 @@ define(function(require) { 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); @@ -1340,7 +1346,9 @@ define(function(require) { var message, parseStub; message = { - from: [{address: 'asdasdasd'}], + from: [{ + address: 'asdasdasd' + }], encrypted: true, decrypted: false, body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' @@ -1353,12 +1361,12 @@ define(function(require) { message: message }, function(error, msg) { expect(error).to.exist; - + 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; diff --git a/test/new-unit/mail-list-ctrl-test.js b/test/new-unit/mail-list-ctrl-test.js index 093f72e..f09e72d 100644 --- a/test/new-unit/mail-list-ctrl-test.js +++ b/test/new-unit/mail-list-ctrl-test.js @@ -225,7 +225,7 @@ define(function(require) { }); }); - describe('getContent', function() { + describe('getBody', function() { it('should get the mail content', function() { scope.state.nav = { currentFolder: { @@ -233,8 +233,8 @@ define(function(require) { } }; - scope.getContent(); - expect(emailDaoMock.getMessageContent.calledOnce).to.be.true; + scope.getBody(); + expect(emailDaoMock.getBody.calledOnce).to.be.true; }); }); From 8d745cb2cfd0261584811bc40c1f9b3e188c6cc7 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 20 Feb 2014 16:11:18 +0100 Subject: [PATCH 09/10] review email dao --- src/js/dao/email-dao.js | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 8998694..5022011 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -209,7 +209,6 @@ define(function(require) { var self = this, folder, isFolderInitialized; - // validate options if (!options.folder) { callback({ @@ -244,6 +243,9 @@ define(function(require) { doLocalDelta(); + /* + * pre-fill the memory with the messages stored on the hard disk + */ function initFolderMessages() { folder.messages = []; self._localListMessages({ @@ -256,7 +258,9 @@ define(function(require) { } storedMessages.forEach(function(storedMessage) { - delete storedMessage.body; // do not flood the memory + // remove the body to not load unnecessary data to memory + delete storedMessage.body; + folder.messages.push(storedMessage); }); @@ -265,6 +269,9 @@ define(function(require) { }); } + /* + * compares the messages in memory to the messages on the disk + */ function doLocalDelta() { self._localListMessages({ folder: folder.path @@ -278,7 +285,8 @@ define(function(require) { doDelta1(); /* - * delta1: storage > memory => we deleted messages, remove from remote + * delta1: + * storage contains messages that are not present in memory => we deleted messages from the memory, so remove the messages from the remote and the disk */ function doDelta1() { var inMemoryUids = _.pluck(folder.messages, 'uid'), @@ -294,7 +302,7 @@ define(function(require) { doDeltaF2(); }); - // deltaF2 contains references to the in-memory messages + // delta1 contains uids of messages on the disk delta1.forEach(function(inMemoryUid) { var deleteMe = { folder: folder.path, @@ -322,10 +330,11 @@ define(function(require) { } /* - * deltaF2: memory > storage => we changed flags, sync them to the remote and memory + * deltaF2: + * memory contains messages that have flags other than those in 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 + var deltaF2 = checkFlags(folder.messages, storedMessages); // deltaF2 contains the message objects, we need those to sync the flags if (_.isEmpty(deltaF2)) { callback(); @@ -377,6 +386,9 @@ define(function(require) { }); } + /* + * compare the messages on the imap server to the in memory messages + */ function doImapDelta() { self._imapSearch({ folder: folder.path @@ -390,7 +402,8 @@ define(function(require) { doDelta3(); /* - * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage + * delta3: + * memory contains messages that are not present on the imap => we deleted messages directly from the remote, remove from memory and storage */ function doDelta3() { var inMemoryUids = _.pluck(folder.messages, 'uid'), @@ -405,9 +418,9 @@ define(function(require) { doDelta4(); }); - // delta3 contains references to the in-memory messages that have been deleted from the remote + // delta3 contains uids of the in-memory messages that have been deleted from the remote delta3.forEach(function(inMemoryUid) { - // remove delta3 from local storage + // remove from local storage self._localDeleteMessage({ folder: folder.path, uid: inMemoryUid @@ -418,9 +431,9 @@ define(function(require) { return; } - // remove the uid from memory + // remove from memory var inMemoryMessage = _.findWhere(folder.messages, function(msg) { - return msg.uid.a === inMemoryUid; + return msg.uid === inMemoryUid; }); folder.messages.splice(folder.messages.indexOf(inMemoryMessage), 1); @@ -429,9 +442,9 @@ define(function(require) { }); } - /* - * delta4: imap > memory => we have new messages available, fetch downstream to memory and storage + * delta4: + * imap contains messages that are not present in memory => we have new messages available, fetch downstream to memory and storage */ function doDelta4() { var inMemoryUids = _.pluck(folder.messages, 'uid'), @@ -444,7 +457,7 @@ define(function(require) { if (!_.isEmpty(inMemoryUids)) { var maxInMemoryUid = Math.max.apply(null, inMemoryUids); // apply works with separate arguments rather than an array - // eliminate everything prior to maxInMemoryUid, that was already synced + // eliminate everything prior to maxInMemoryUid, i.e. everything that was already synced delta4 = _.filter(delta4, function(uid) { return uid > maxInMemoryUid; }); From 4b6d2cdea9b50a5b130851ea7a73bcf704380010 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 20 Feb 2014 16:13:10 +0100 Subject: [PATCH 10/10] use master branch of imap client --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80488d8..8f17a48 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/dev/stream-plaintext", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/master", "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master", "requirejs": "2.1.10" },