fix unread and answered flags are updated

This commit is contained in:
Felix Hammerl 2013-12-05 18:28:18 +01:00
parent f3ef8fdf91
commit b4115ed879
6 changed files with 397 additions and 117 deletions

View File

@ -60,9 +60,16 @@ define(function(require) {
}
$scope.state.mailList.selected = email;
$scope.state.read.toggle(true);
// // mark selected message as 'read'
// markAsRead(email);
// if the email is unread, please sync the new state.
// otherweise forget about it.
if (!email.unread) {
return;
}
email.unread = false;
$scope.synchronize();
};
$scope.synchronize = function(callback) {
@ -204,34 +211,6 @@ define(function(require) {
function getFolder() {
return $scope.state.nav.currentFolder;
}
// function markAsRead(email) {
// // marking mails as read is meaningless in the outbox
// if (getFolder().type === 'Outbox') {
// return;
// }
// $scope.state.read.toggle(true);
// if (!window.chrome || !chrome.socket) {
// return;
// }
// if (!email.unread) {
// return;
// }
// email.unread = false;
// emailDao.imapMarkMessageRead({
// folder: getFolder().path,
// uid: email.uid
// }, function(err) {
// if (err) {
// updateStatus('Error marking read!');
// $scope.onError(err);
// return;
// }
// });
// }
};
function createDummyMails() {

View File

@ -181,12 +181,6 @@ define(function(require) {
// mark list object
$scope.replyTo.answered = true;
// mark remote imap object
emailDao.markAnswered({
uid: $scope.replyTo.uid,
folder: $scope.state.nav.currentFolder.path
}, $scope.onError);
}
};

View File

@ -143,15 +143,26 @@ define(function(require) {
EmailDAO.prototype.sync = function(options, callback) {
/*
* Here's how delta sync works:
* delta1: storage > memory => we deleted messages, remove from remote
*
* First, we sync the messages between memory and local storage, based on their uid
* delta1: storage > memory => we deleted messages, remove from remote and memory
* delta2: memory > storage => we added messages, push to remote <<< not supported yet
* 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
*
* Second, we check the delta for the flags
* deltaF1: memory > storage => we changed flags, sync them to the remote and memory
*
* Third, we go on to sync between imap and memory, again based on uid
* 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
*
* Fourth, we pull changes in the flags downstream
* deltaF2: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
*/
var self = this,
folder,
delta1 /*, delta2 */ , delta3, delta4,
delta1 /*, delta2 */ , delta3, delta4, //message
deltaF1, deltaF2,
isFolderInitialized;
@ -237,24 +248,22 @@ define(function(require) {
/*
* delta1: storage > memory => we deleted messages, remove from remote
* delta2: memory > storage => we added messages, push to remote
* deltaF1: memory > storage => we changed flags, sync them to the remote and memory
*/
delta1 = checkDelta(messages, folder.messages);
// delta2 = checkDelta(folder.messages, messages); // not supported yet
if (_.isEmpty(delta1) /* && _.isEmpty(delta2)*/ ) {
// if there is no delta, head directly to imap sync
callback();
doImapDelta();
return;
}
deltaF1 = checkFlags(folder.messages, messages);
doDelta1();
function doDelta1() {
if (_.isEmpty(delta1)) {
doDeltaF1();
return;
}
var after = _.after(delta1.length, function() {
// doDelta2(); when it is implemented
callback();
doImapDelta();
doDeltaF1();
});
delta1.forEach(function(message) {
@ -282,6 +291,47 @@ define(function(require) {
});
});
}
function doDeltaF1() {
if (_.isEmpty(deltaF1)) {
callback();
doImapDelta();
return;
}
var after = _.after(deltaF1.length, function() {
callback();
doImapDelta();
});
deltaF1.forEach(function(message) {
self._mark({
folder: folder.path,
uid: message.uid,
unread: message.unread,
answered: message.answered
}, function(err) {
if (err) {
self._account.busy = false;
callback(err);
return;
}
self._localStoreMessages({
folder: folder.path,
emails: [message]
}, function(err) {
if (err) {
self._account.busy = false;
callback(err);
return;
}
after();
});
});
});
}
});
}
@ -304,18 +354,13 @@ define(function(require) {
});
/*
* 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: 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
* deltaF2: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
*/
delta3 = checkDelta(folder.messages, headers);
delta4 = checkDelta(headers, folder.messages);
if (_.isEmpty(delta3) && _.isEmpty(delta4)) {
// if there is no delta, we're done
self._account.busy = false;
callback();
return;
}
deltaF2 = checkFlags(headers, folder.messages);
doDelta3();
@ -357,13 +402,11 @@ define(function(require) {
function doDelta4() {
// no delta, we're done here
if (_.isEmpty(delta4)) {
self._account.busy = false;
callback();
doDeltaF2();
}
var after = _.after(delta4.length, function() {
self._account.busy = false;
callback();
doDeltaF2();
});
delta4.forEach(function(header) {
@ -391,6 +434,9 @@ define(function(require) {
return;
}
message.answered = header.answered;
message.unread = header.unread;
// add the encrypted message to the local storage
self._localStoreMessages({
folder: folder.path,
@ -417,6 +463,44 @@ define(function(require) {
});
});
}
// we have a mismatch concerning flags between imap and memory.
// pull changes from imap.
function doDeltaF2() {
if (_.isEmpty(deltaF2)) {
self._account.busy = false;
callback();
return;
}
var after = _.after(deltaF2.length, function() {
self._account.busy = false;
callback();
});
deltaF2.forEach(function(header) {
// we don't work on the header, we work on the live object
var msg = _.findWhere(folder.messages, {
uid: header.uid
});
msg.unread = header.unread;
msg.answered = header.answered;
self._localStoreMessages({
folder: folder.path,
emails: [msg]
}, function(err) {
if (err) {
self._account.busy = false;
callback(err);
return;
}
after();
});
});
}
});
}
@ -441,6 +525,27 @@ define(function(require) {
return delta;
}
/*
* checks if there are some flags that have changed in a and b
*/
function checkFlags(a, b) {
var i, aI, bI,
delta = [];
// find the delta
for (i = a.length - 1; i >= 0; i--) {
aI = a[i];
bI = _.findWhere(b, {
uid: aI.uid
});
if (bI && (aI.unread !== bI.unread || aI.answered !== bI.answered)) {
delta.push(aI);
}
}
return delta;
}
function isVerificationMail(email) {
return email.subject === str.subjectPrefix + str.verificationSubject;
}
@ -472,9 +577,10 @@ define(function(require) {
}
// public key has been verified, mark the message as read, delete it, and ignore it in the future
self.markRead({
self._mark({
folder: options.folder,
uid: email.uid
uid: email.uid,
unread: false
}, function(err) {
if (err) {
localCallback(err);
@ -551,19 +657,12 @@ define(function(require) {
}
};
EmailDAO.prototype.markRead = function(options, callback) {
EmailDAO.prototype._mark = function(options, callback) {
this._imapClient.updateFlags({
path: options.folder,
uid: options.uid,
unread: false
}, callback);
};
EmailDAO.prototype.markAnswered = function(options, callback) {
this._imapClient.updateFlags({
path: options.folder,
uid: options.uid,
answered: true
unread: options.unread,
answered: options.answered
}, callback);
};

View File

@ -31,7 +31,9 @@ define(function(require) {
address: 'qwe@qwe.de'
}],
subject: '[whiteout] qweasd',
body: '-----BEGIN PGP MESSAGE-----\nasd\n-----END PGP MESSAGE-----'
body: '-----BEGIN PGP MESSAGE-----\nasd\n-----END PGP MESSAGE-----',
unread: false,
answered: false
};
verificationUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!';
verificationMail = {
@ -44,7 +46,8 @@ define(function(require) {
}], // list of receivers
subject: "[whiteout] New public key uploaded", // Subject line
body: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, // plaintext body
unread: true
unread: true,
answered: false
};
dummyDecryptedMail = {
uid: 1234,
@ -55,7 +58,9 @@ define(function(require) {
address: 'qwe@qwe.de'
}],
subject: 'qweasd',
body: 'asd'
body: 'asd',
unread: false,
answered: false
};
nonWhitelistedMail = {
uid: 1234,
@ -113,7 +118,7 @@ define(function(require) {
expect(obj).to.equal(o);
done();
};
dao._imapClient.onIncomingMessage(o);
});
});
@ -594,7 +599,9 @@ define(function(require) {
folder: folder
}).yields(null, [{
uid: dummyEncryptedMail.uid,
subject: '[whiteout] ' + dummyEncryptedMail // the object has already been manipulated as a side-effect...
subject: '[whiteout] ' + dummyEncryptedMail.subject, // the object has already been manipulated as a side-effect...
unread: dummyEncryptedMail.unread,
answered: dummyEncryptedMail.answered
}]);
dao.sync({
@ -1234,9 +1241,10 @@ define(function(require) {
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail);
keychainStub.verifyPublicKey.withArgs(verificationUuid).yields();
markReadStub = sinon.stub(dao, 'markRead').withArgs({
markReadStub = sinon.stub(dao, '_mark').withArgs({
folder: folder,
uid: verificationMail.uid
uid: verificationMail.uid,
unread: false
}).yields();
imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').withArgs({
folder: folder,
@ -1283,7 +1291,7 @@ define(function(require) {
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail);
keychainStub.verifyPublicKey.yields();
markReadStub = sinon.stub(dao, 'markRead').yields();
markReadStub = sinon.stub(dao, '_mark').yields();
imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({});
dao.sync({
@ -1326,7 +1334,7 @@ define(function(require) {
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail);
keychainStub.verifyPublicKey.yields();
markReadStub = sinon.stub(dao, 'markRead').yields({});
markReadStub = sinon.stub(dao, '_mark').yields({});
imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage');
dao.sync({
@ -1369,7 +1377,7 @@ define(function(require) {
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail);
keychainStub.verifyPublicKey.yields({});
markReadStub = sinon.stub(dao, 'markRead');
markReadStub = sinon.stub(dao, '_mark');
imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage');
dao.sync({
@ -1413,7 +1421,7 @@ define(function(require) {
localListStub = sinon.stub(dao, '_localListMessages').yields(null, []);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail);
markReadStub = sinon.stub(dao, 'markRead');
markReadStub = sinon.stub(dao, '_mark');
imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage');
dao.sync({
@ -1456,7 +1464,7 @@ define(function(require) {
localListStub = sinon.stub(dao, '_localListMessages').yields(null, []);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail);
markReadStub = sinon.stub(dao, 'markRead');
markReadStub = sinon.stub(dao, '_mark');
imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage');
dao.sync({
@ -1482,38 +1490,239 @@ define(function(require) {
done();
});
});
});
describe('markAsRead', function() {
it('should work', function(done) {
imapClientStub.updateFlags.withArgs({
path: 'asdf',
uid: 1,
unread: false
it('should sync tags from memory to imap and storage', function(done) {
var folder, localListStub, imapListStub, invocations,
markStub, localStoreStub;
invocations = 0;
folder = 'FOLDAAAA';
dao._account.folders = [{
type: 'Folder',
path: folder,
messages: [dummyDecryptedMail]
}];
var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail));
var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail));
dummyDecryptedMail.unread = inImap.unread = true;
localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [inImap]);
markStub = sinon.stub(dao, '_mark').withArgs({
folder: folder,
uid: dummyDecryptedMail.uid,
unread: dummyDecryptedMail.unread,
answered: dummyDecryptedMail.answered
}).yields();
localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({
folder: folder,
emails: [dummyDecryptedMail]
}).yields();
dao.markRead({
folder: 'asdf',
uid: 1
dao.sync({
folder: folder
}, function(err) {
expect(imapClientStub.updateFlags.calledOnce).to.be.true;
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]).to.not.be.empty;
expect(localListStub.calledOnce).to.be.true;
expect(imapListStub.calledOnce).to.be.true;
expect(markStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
done();
});
});
it('should error while syncing tags from memory to storage', function(done) {
var folder, localListStub, imapListStub, invocations,
markStub, localStoreStub;
invocations = 0;
folder = 'FOLDAAAA';
dao._account.folders = [{
type: 'Folder',
path: folder,
messages: [dummyDecryptedMail]
}];
var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail));
var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail));
dummyDecryptedMail.unread = inImap.unread = true;
localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [inImap]);
markStub = sinon.stub(dao, '_mark').yields();
localStoreStub = sinon.stub(dao, '_localStoreMessages').yields({});
dao.sync({
folder: folder
}, function(err) {
expect(err).to.exist;
expect(dao._account.busy).to.be.false;
expect(dao._account.folders[0]).to.not.be.empty;
expect(localListStub.calledOnce).to.be.true;
expect(markStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
expect(imapListStub.called).to.be.false;
done();
});
});
it('should error while syncing tags from memory to imap', function(done) {
var folder, localListStub, imapListStub, invocations,
markStub, localStoreStub;
invocations = 0;
folder = 'FOLDAAAA';
dao._account.folders = [{
type: 'Folder',
path: folder,
messages: [dummyDecryptedMail]
}];
var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail));
var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail));
dummyDecryptedMail.unread = inImap.unread = true;
localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [inImap]);
markStub = sinon.stub(dao, '_mark').yields({});
localStoreStub = sinon.stub(dao, '_localStoreMessages');
dao.sync({
folder: folder
}, function(err) {
expect(err).to.exist;
expect(dao._account.busy).to.be.false;
expect(dao._account.folders[0]).to.not.be.empty;
expect(localListStub.calledOnce).to.be.true;
expect(markStub.calledOnce).to.be.true;
expect(localStoreStub.called).to.be.false;
expect(imapListStub.called).to.be.false;
done();
});
});
it('should sync tags from imap to memory and storage', function(done) {
var folder, localListStub, imapListStub, invocations,
markStub, localStoreStub;
invocations = 0;
folder = 'FOLDAAAA';
dao._account.folders = [{
type: 'Folder',
path: folder,
messages: [dummyDecryptedMail]
}];
var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail));
var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail));
dummyDecryptedMail.unread = inStorage.unread = true;
localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [inImap]);
markStub = sinon.stub(dao, '_mark');
localStoreStub = sinon.stub(dao, '_localStoreMessages').withArgs({
folder: folder,
emails: [dummyDecryptedMail]
}).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]).to.not.be.empty;
expect(localListStub.calledOnce).to.be.true;
expect(imapListStub.calledOnce).to.be.true;
expect(markStub.called).to.be.false;
expect(localStoreStub.calledOnce).to.be.true;
expect(dummyDecryptedMail.unread).to.equal(inImap.unread);
done();
});
});
it('should error while syncing tags from imap to storage', function(done) {
var folder, localListStub, imapListStub, invocations,
markStub, localStoreStub;
invocations = 0;
folder = 'FOLDAAAA';
dao._account.folders = [{
type: 'Folder',
path: folder,
messages: [dummyDecryptedMail]
}];
var inStorage = JSON.parse(JSON.stringify(dummyEncryptedMail));
var inImap = JSON.parse(JSON.stringify(dummyEncryptedMail));
dummyDecryptedMail.unread = inStorage.unread = true;
localListStub = sinon.stub(dao, '_localListMessages').yields(null, [inStorage]);
imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [inImap]);
markStub = sinon.stub(dao, '_mark');
localStoreStub = sinon.stub(dao, '_localStoreMessages').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]).to.not.be.empty;
expect(localListStub.calledOnce).to.be.true;
expect(imapListStub.calledOnce).to.be.true;
expect(markStub.called).to.be.false;
expect(localStoreStub.calledOnce).to.be.true;
expect(dummyDecryptedMail.unread).to.equal(inImap.unread);
done();
});
});
});
describe('markAsAnswered', function() {
describe('mark', function() {
it('should work', function(done) {
imapClientStub.updateFlags.withArgs({
path: 'asdf',
uid: 1,
answered: true
unread: false,
answered: false
}).yields();
dao.markAnswered({
dao._mark({
folder: 'asdf',
uid: 1
uid: 1,
unread: false,
answered: false
}, function(err) {
expect(imapClientStub.updateFlags.calledOnce).to.be.true;
expect(err).to.not.exist;

View File

@ -75,7 +75,11 @@ define(function(require) {
mocks.module('maillisttest');
mocks.inject(function($rootScope, $controller) {
scope = $rootScope.$new();
scope.state = {};
scope.state = {
read: {
toggle: function() {}
}
};
ctrl = $controller(MailListCtrl, {
$scope: scope
});
@ -210,11 +214,12 @@ define(function(require) {
scope.state.nav = {
currentFolder: currentFolder
};
scope.state.mailList.selected = undefined;
scope.synchronize();
// emails array is also used as the outbox's pending mail
expect(scope.state.mailList.selected).to.deep.equal(emails[0]);
expect(scope.state.mailList.selected).to.deep.equal(emails[emails.length - 1]);
});
});

View File

@ -141,7 +141,7 @@ define(function(require) {
});
describe('send to outbox', function() {
it('should work', function(done) {
it('should work', function() {
var verifyToSpy = sinon.spy(scope, 'verifyTo'),
re = {
from: [{
@ -159,21 +159,15 @@ define(function(require) {
scope.emptyOutbox = function() {};
emailDaoMock.store.yields();
emailDaoMock.markAnswered.yields();
scope.onError = function(err) {
expect(err).to.not.exist;
expect(scope.state.writer.open).to.be.false;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(verifyToSpy.calledOnce).to.be.true;
scope.verifyTo.restore();
done();
};
scope.state.writer.write(re);
scope.sendToOutbox();
expect(scope.state.writer.open).to.be.false;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(verifyToSpy.calledOnce).to.be.true;
scope.verifyTo.restore();
});
it('should not work and not close the write view', function(done) {