1
0
mirror of https://github.com/moparisthebest/mail synced 2025-01-30 22:50:17 -05:00

Merge remote-tracking branch 'origin/dev/sync_flags'

This commit is contained in:
Tankred Hase 2013-12-05 19:15:36 +01:00
commit 4cf8e3cb5c
8 changed files with 414 additions and 115 deletions

View File

@ -35,7 +35,9 @@ define(function(require) {
$scope.setState(states.PROCESSING);
setTimeout(function() {
emailDao.unlock({}, passphrase, function(err) {
emailDao.unlock({
passphrase: passphrase
}, function(err) {
if (err) {
$scope.setState(states.IDLE);
$scope.onError(err);

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) {
@ -209,34 +216,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

@ -167,11 +167,11 @@ define(function(require) {
$scope.$apply();
$scope.emptyOutbox($scope.onOutboxUpdate);
markAnwsered();
markAnswered();
});
};
function markAnwsered() {
function markAnswered() {
// mark replyTo as answered
if (!$scope.replyTo) {
return;
@ -179,10 +179,7 @@ define(function(require) {
// mark list object
$scope.replyTo.answered = true;
// mark remote imap object
emailDao.markAnswered({
uid: $scope.replyTo.uid,
emailDao.sync({
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;
@ -238,24 +249,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) {
@ -283,6 +292,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._imapMark({
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();
});
});
});
}
});
}
@ -305,18 +355,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();
@ -358,13 +403,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) {
@ -392,10 +435,17 @@ define(function(require) {
return;
}
// create a bastard child of smtp and imap.
// before thinking this is stupid, talk to the guys who wrote this.
header.id = message.id;
header.body = message.body;
header.html = message.html;
header.attachments = message.attachments;
// add the encrypted message to the local storage
self._localStoreMessages({
folder: folder.path,
emails: [message]
emails: [header]
}, function(err) {
if (err) {
self._account.busy = false;
@ -404,7 +454,7 @@ define(function(require) {
}
// decrypt and add to folder in memory
handleMessage(message, function(err, cleartextMessage) {
handleMessage(header, function(err, cleartextMessage) {
if (err) {
self._account.busy = false;
callback(err);
@ -418,6 +468,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();
});
});
}
});
}
@ -442,6 +530,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;
}
@ -473,9 +582,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._imapMark({
folder: options.folder,
uid: email.uid
uid: email.uid,
unread: false
}, function(err) {
if (err) {
localCallback(err);
@ -552,19 +662,12 @@ define(function(require) {
}
};
EmailDAO.prototype.markRead = function(options, callback) {
EmailDAO.prototype._imapMark = 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, '_imapMark').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, '_imapMark').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, '_imapMark').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, '_imapMark');
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, '_imapMark');
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, '_imapMark');
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, '_imapMark').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, '_imapMark').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, '_imapMark').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, '_imapMark');
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, '_imapMark');
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._imapMark({
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

@ -67,7 +67,9 @@ define(function(require) {
it('should unlock crypto', function(done) {
scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase;
emailDaoMock.unlock.withArgs({}, passphrase).yields();
emailDaoMock.unlock.withArgs({
passphrase: passphrase
}).yields();
setStateStub = sinon.stub(scope, 'setState', function(state) {
if (setStateStub.calledOnce) {
expect(state).to.equal(2);
@ -92,7 +94,9 @@ define(function(require) {
it('should not work when keypair generation fails', function(done) {
scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase;
emailDaoMock.unlock.withArgs({}, passphrase).yields(new Error('asd'));
emailDaoMock.unlock.withArgs({
passphrase: passphrase
}).yields(new Error('asd'));
setStateStub = sinon.stub(scope, 'setState', function(state) {
if (setStateStub.calledOnce) {
expect(state).to.equal(2);

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

@ -157,23 +157,23 @@ 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;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
scope.verifyTo.restore();
done();
};
emailDaoMock.store.yields();
emailDaoMock.sync.yields();
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should not work and not close the write view', function(done) {