mirror of
https://github.com/moparisthebest/mail
synced 2024-11-23 09:22:23 -05:00
Merge pull request #21 from whiteout-io/dev/stream-plaintext
Dev/stream plaintext
This commit is contained in:
commit
47de4ed5d0
@ -58,15 +58,41 @@ define(function(require) {
|
|||||||
// scope functions
|
// scope functions
|
||||||
//
|
//
|
||||||
|
|
||||||
|
$scope.getBody = function(email) {
|
||||||
|
// don't stream message content of outbox messages...
|
||||||
|
if (getFolder().type === 'Outbox') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emailDao.getBody({
|
||||||
|
folder: getFolder().path,
|
||||||
|
message: email
|
||||||
|
}, function(error) {
|
||||||
|
$scope.$apply();
|
||||||
|
$scope.onError(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when clicking on an email list item
|
* Called when clicking on an email list item
|
||||||
*/
|
*/
|
||||||
$scope.select = function(email) {
|
$scope.select = function(email) {
|
||||||
|
// unselect an item
|
||||||
if (!email) {
|
if (!email) {
|
||||||
$scope.state.mailList.selected = undefined;
|
$scope.state.mailList.selected = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.mailList.selected = email;
|
||||||
$scope.state.read.toggle(true);
|
$scope.state.read.toggle(true);
|
||||||
|
|
||||||
@ -279,10 +305,88 @@ define(function(require) {
|
|||||||
}]; // list of receivers
|
}]; // list of receivers
|
||||||
if (attachments) {
|
if (attachments) {
|
||||||
// body structure with three 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.bodystructure = {
|
||||||
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}];
|
"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 {
|
} 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.attachments = [];
|
||||||
}
|
}
|
||||||
this.unread = unread;
|
this.unread = unread;
|
||||||
@ -306,14 +410,48 @@ define(function(require) {
|
|||||||
ngModule.directive('ngIscroll', function() {
|
ngModule.directive('ngIscroll', function() {
|
||||||
return {
|
return {
|
||||||
link: function(scope, elm, attrs) {
|
link: function(scope, elm, attrs) {
|
||||||
var model = attrs.ngIscroll;
|
var model = attrs.ngIscroll,
|
||||||
|
listEl = elm[0];
|
||||||
|
|
||||||
scope.$watch(model, function() {
|
scope.$watch(model, function() {
|
||||||
var myScroll;
|
var myScroll;
|
||||||
// activate iscroll
|
// activate iscroll
|
||||||
myScroll = new IScroll(elm[0], {
|
myScroll = new IScroll(listEl, {
|
||||||
mouseWheel: true
|
mouseWheel: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// load the visible message bodies, when the list is re-initialized and when scrolling stopped
|
||||||
|
loadVisible();
|
||||||
|
myScroll.on('scrollEnd', loadVisible);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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--) {
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) {
|
||||||
|
scope.getBody(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -207,11 +207,7 @@ define(function(require) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var self = this,
|
var self = this,
|
||||||
folder,
|
folder, isFolderInitialized;
|
||||||
delta1 /*, delta2 */ , delta3, delta4, //message
|
|
||||||
deltaF2, deltaF4,
|
|
||||||
isFolderInitialized;
|
|
||||||
|
|
||||||
|
|
||||||
// validate options
|
// validate options
|
||||||
if (!options.folder) {
|
if (!options.folder) {
|
||||||
@ -247,6 +243,9 @@ define(function(require) {
|
|||||||
|
|
||||||
doLocalDelta();
|
doLocalDelta();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* pre-fill the memory with the messages stored on the hard disk
|
||||||
|
*/
|
||||||
function initFolderMessages() {
|
function initFolderMessages() {
|
||||||
folder.messages = [];
|
folder.messages = [];
|
||||||
self._localListMessages({
|
self._localListMessages({
|
||||||
@ -258,33 +257,21 @@ define(function(require) {
|
|||||||
return;
|
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) {
|
storedMessages.forEach(function(storedMessage) {
|
||||||
handleMessage(storedMessage, function(err, cleartextMessage) {
|
// remove the body to not load unnecessary data to memory
|
||||||
if (err) {
|
delete storedMessage.body;
|
||||||
self._account.busy = false;
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.messages.push(cleartextMessage);
|
folder.messages.push(storedMessage);
|
||||||
after();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
callback();
|
||||||
|
doImapDelta();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* compares the messages in memory to the messages on the disk
|
||||||
|
*/
|
||||||
function doLocalDelta() {
|
function doLocalDelta() {
|
||||||
self._localListMessages({
|
self._localListMessages({
|
||||||
folder: folder.path
|
folder: folder.path
|
||||||
@ -295,18 +282,17 @@ define(function(require) {
|
|||||||
return;
|
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();
|
doDelta1();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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() {
|
function doDelta1() {
|
||||||
|
var inMemoryUids = _.pluck(folder.messages, 'uid'),
|
||||||
|
storedMessageUids = _.pluck(storedMessages, 'uid'),
|
||||||
|
delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids
|
||||||
|
|
||||||
if (_.isEmpty(delta1)) {
|
if (_.isEmpty(delta1)) {
|
||||||
doDeltaF2();
|
doDeltaF2();
|
||||||
return;
|
return;
|
||||||
@ -316,11 +302,11 @@ define(function(require) {
|
|||||||
doDeltaF2();
|
doDeltaF2();
|
||||||
});
|
});
|
||||||
|
|
||||||
// deltaF2 contains references to the in-memory messages
|
// delta1 contains uids of messages on the disk
|
||||||
delta1.forEach(function(inMemoryMessage) {
|
delta1.forEach(function(inMemoryUid) {
|
||||||
var deleteMe = {
|
var deleteMe = {
|
||||||
folder: folder.path,
|
folder: folder.path,
|
||||||
uid: inMemoryMessage.uid
|
uid: inMemoryUid
|
||||||
};
|
};
|
||||||
|
|
||||||
self._imapDeleteMessage(deleteMe, function(err) {
|
self._imapDeleteMessage(deleteMe, function(err) {
|
||||||
@ -343,7 +329,13 @@ define(function(require) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* deltaF2:
|
||||||
|
* memory contains messages that have flags other than those in storage => we changed flags, sync them to the remote and memory
|
||||||
|
*/
|
||||||
function doDeltaF2() {
|
function doDeltaF2() {
|
||||||
|
var deltaF2 = checkFlags(folder.messages, storedMessages); // deltaF2 contains the message objects, we need those to sync the flags
|
||||||
|
|
||||||
if (_.isEmpty(deltaF2)) {
|
if (_.isEmpty(deltaF2)) {
|
||||||
callback();
|
callback();
|
||||||
doImapDelta();
|
doImapDelta();
|
||||||
@ -394,51 +386,44 @@ define(function(require) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* compare the messages on the imap server to the in memory messages
|
||||||
|
*/
|
||||||
function doImapDelta() {
|
function doImapDelta() {
|
||||||
self._imapSearch({
|
self._imapSearch({
|
||||||
folder: folder.path
|
folder: folder.path
|
||||||
}, function(err, uids) {
|
}, function(err, inImapUids) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._account.busy = false;
|
self._account.busy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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();
|
doDelta3();
|
||||||
|
|
||||||
// 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() {
|
function doDelta3() {
|
||||||
|
var inMemoryUids = _.pluck(folder.messages, 'uid'),
|
||||||
|
delta3 = _.difference(inMemoryUids, inImapUids);
|
||||||
|
|
||||||
if (_.isEmpty(delta3)) {
|
if (_.isEmpty(delta3)) {
|
||||||
doDelta4();
|
doDelta4();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var after = _.after(delta3.length, function() {
|
var after = _.after(delta3.length, function() {
|
||||||
// we're done with delta 3, so let's continue
|
|
||||||
doDelta4();
|
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(inMemoryMessage) {
|
delta3.forEach(function(inMemoryUid) {
|
||||||
// remove delta3 from local storage
|
// remove from local storage
|
||||||
self._localDeleteMessage({
|
self._localDeleteMessage({
|
||||||
folder: folder.path,
|
folder: folder.path,
|
||||||
uid: inMemoryMessage.uid
|
uid: inMemoryUid
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._account.busy = false;
|
self._account.busy = false;
|
||||||
@ -446,85 +431,98 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove delta3 from memory
|
// remove from memory
|
||||||
var idx = folder.messages.indexOf(inMemoryMessage);
|
var inMemoryMessage = _.findWhere(folder.messages, function(msg) {
|
||||||
folder.messages.splice(idx, 1);
|
return msg.uid === inMemoryUid;
|
||||||
|
});
|
||||||
|
folder.messages.splice(folder.messages.indexOf(inMemoryMessage), 1);
|
||||||
|
|
||||||
after();
|
after();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have new messages available, fetch to memory and storage
|
/*
|
||||||
// (downstream sync)
|
* delta4:
|
||||||
|
* imap contains messages that are not present in memory => we have new messages available, fetch downstream to memory and storage
|
||||||
|
*/
|
||||||
function doDelta4() {
|
function doDelta4() {
|
||||||
|
var inMemoryUids = _.pluck(folder.messages, 'uid'),
|
||||||
|
delta4 = _.difference(inImapUids, inMemoryUids);
|
||||||
|
|
||||||
// eliminate uids smaller than the biggest local uid, i.e. just fetch everything
|
// 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
|
// 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
|
// 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.
|
// uid smaller than anything we've encountered before.
|
||||||
if (!_.isEmpty(folder.messages)) {
|
if (!_.isEmpty(inMemoryUids)) {
|
||||||
var localUids = _.pluck(folder.messages, 'uid'),
|
var maxInMemoryUid = Math.max.apply(null, inMemoryUids); // apply works with separate arguments rather than an array
|
||||||
maxLocalUid = Math.max.apply(null, localUids);
|
|
||||||
|
|
||||||
// eliminate everything prior to maxLocalUid
|
// eliminate everything prior to maxInMemoryUid, i.e. everything that was already synced
|
||||||
delta4 = _.filter(delta4, function(uidWrapper) {
|
delta4 = _.filter(delta4, function(uid) {
|
||||||
return uidWrapper.uid > maxLocalUid;
|
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
|
// no delta, we're done here
|
||||||
if (_.isEmpty(delta4)) {
|
if (_.isEmpty(delta4)) {
|
||||||
doDeltaF4();
|
doDeltaF4();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// delta4 contains the headers that are newly available on the remote
|
self._imapListMessages({
|
||||||
var nextUidWrapper = delta4.shift();
|
|
||||||
|
|
||||||
// get the whole message
|
|
||||||
self._imapGetMessage({
|
|
||||||
folder: folder.path,
|
folder: folder.path,
|
||||||
uid: nextUidWrapper.uid
|
firstUid: Math.min.apply(null, delta4),
|
||||||
}, function(err, message) {
|
lastUid: Math.max.apply(null, delta4)
|
||||||
|
}, function(err, messages) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._account.busy = false;
|
self._account.busy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// imap filtering is insufficient, since google ignores non-alphabetical characters
|
// if there is a verification message in the synced messages, handle it
|
||||||
if (message.subject.indexOf(str.subjectPrefix) === -1) {
|
var verificationMessage = _.findWhere(messages, {
|
||||||
syncNextItem();
|
subject: str.subjectPrefix + str.verificationSubject
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (isVerificationMail(message)) {
|
if (verificationMessage) {
|
||||||
verify(message, function(err) {
|
handleVerification(verificationMessage, function(err) {
|
||||||
|
// TODO: show usable error when the verification failed
|
||||||
if (err) {
|
if (err) {
|
||||||
self._account.busy = false;
|
self._account.busy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncNextItem();
|
storeHeaders();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the encrypted message to the local storage
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out the "[whiteout] " prefix
|
||||||
|
messages.forEach(function(messages) {
|
||||||
|
messages.subject = messages.subject.replace(/^\[whiteout\] /, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// persist the encrypted message to the local storage
|
||||||
self._localStoreMessages({
|
self._localStoreMessages({
|
||||||
folder: folder.path,
|
folder: folder.path,
|
||||||
emails: [message]
|
emails: messages
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._account.busy = false;
|
self._account.busy = false;
|
||||||
@ -532,25 +530,21 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt and add to folder in memory
|
// if persisting worked, add them to the messages array
|
||||||
handleMessage(message, function(err, cleartextMessage) {
|
folder.messages = folder.messages.concat(messages);
|
||||||
if (err) {
|
doDeltaF4();
|
||||||
self._account.busy = false;
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.messages.push(cleartextMessage);
|
|
||||||
syncNextItem();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
|
||||||
|
*/
|
||||||
function doDeltaF4() {
|
function doDeltaF4() {
|
||||||
var answeredUids, unreadUids;
|
var answeredUids, unreadUids,
|
||||||
|
deltaF4 = [];
|
||||||
|
|
||||||
getUnreadUids();
|
getUnreadUids();
|
||||||
|
|
||||||
@ -593,9 +587,6 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFlags() {
|
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) {
|
folder.messages.forEach(function(msg) {
|
||||||
// if the message's uid is among the uids that should be unread,
|
// 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
|
// AND the message is not unread, we clearly have to change that
|
||||||
@ -686,27 +677,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
|
* checks if there are some flags that have changed in a and b
|
||||||
*/
|
*/
|
||||||
@ -728,30 +698,30 @@ define(function(require) {
|
|||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVerificationMail(email) {
|
function handleVerification(message, localCallback) {
|
||||||
return email.subject === str.subjectPrefix + str.verificationSubject;
|
self._imapStreamText({
|
||||||
}
|
folder: options.folder,
|
||||||
|
message: message
|
||||||
|
}, function(error) {
|
||||||
|
var verificationUrlPrefix = config.cloudUrl + config.verificationUrl,
|
||||||
|
uuid, isValidUuid, index;
|
||||||
|
|
||||||
function verify(email, localCallback) {
|
if (error) {
|
||||||
var uuid, isValidUuid, index, verifyUrlPrefix = config.cloudUrl + config.verificationUrl;
|
localCallback(error);
|
||||||
|
|
||||||
if (!email.unread) {
|
|
||||||
// don't bother if the email was already marked as read
|
|
||||||
localCallback();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
index = email.body.indexOf(verifyUrlPrefix);
|
index = message.body.indexOf(verificationUrlPrefix);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
// there's no url in the email, so forget about that.
|
// there's no url in the message, so forget about that.
|
||||||
localCallback();
|
localCallback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid = email.body.substr(index + verifyUrlPrefix.length, config.verificationUuidLength);
|
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);
|
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) {
|
if (!isValidUuid) {
|
||||||
// there's no valid uuid in the email, so forget about that, too.
|
// there's no valid uuid in the message, so forget about that, too.
|
||||||
localCallback();
|
localCallback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -764,108 +734,190 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// public key has been verified, mark the message as read, delete it, and ignore it in the future
|
// public key has been verified, delete the message
|
||||||
self._imapMark({
|
|
||||||
folder: options.folder,
|
|
||||||
uid: email.uid,
|
|
||||||
unread: false
|
|
||||||
}, function(err) {
|
|
||||||
if (err) {
|
|
||||||
localCallback(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self._imapDeleteMessage({
|
self._imapDeleteMessage({
|
||||||
folder: options.folder,
|
folder: options.folder,
|
||||||
uid: email.uid
|
uid: message.uid
|
||||||
}, localCallback);
|
}, 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.getBody = function(options, callback) {
|
||||||
|
var self = this,
|
||||||
|
message = options.message,
|
||||||
|
folder = options.folder;
|
||||||
|
|
||||||
if (containsArmoredCiphertext(message)) {
|
if (message.loadingBody) {
|
||||||
decrypt(message, localCallback);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleartext mail
|
// the message already has a body, so no need to become active here
|
||||||
localCallback(null, message);
|
if (message.body) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function containsArmoredCiphertext(email) {
|
message.loadingBody = true;
|
||||||
return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrypt(email, localCallback) {
|
// the mail does not have its content in memory
|
||||||
var sender;
|
readFromDevice();
|
||||||
|
|
||||||
extractArmoredContent(email);
|
// if possible, read the message body from the device
|
||||||
|
function readFromDevice() {
|
||||||
|
self._localListMessages({
|
||||||
|
folder: folder,
|
||||||
|
uid: message.uid
|
||||||
|
}, function(err, localMessages) {
|
||||||
|
var localMessage;
|
||||||
|
|
||||||
// fetch public key required to verify signatures
|
|
||||||
sender = email.from[0].address;
|
|
||||||
self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
localCallback(err);
|
message.loadingBody = false;
|
||||||
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!senderPubkey) {
|
localMessage = localMessages[0];
|
||||||
|
|
||||||
|
if (!localMessage.body) {
|
||||||
|
streamFromImap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attach the body to the mail object
|
||||||
|
message.body = localMessage.body;
|
||||||
|
handleEncryptedContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if reading the message body from the device was unsuccessful,
|
||||||
|
// stream the message from the imap server
|
||||||
|
function streamFromImap() {
|
||||||
|
self._imapStreamText({
|
||||||
|
folder: folder,
|
||||||
|
message: message
|
||||||
|
}, function(error) {
|
||||||
|
if (error) {
|
||||||
|
message.loadingBody = false;
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.loadingBody = false;
|
||||||
|
|
||||||
|
self._localStoreMessages({
|
||||||
|
folder: folder,
|
||||||
|
emails: [message]
|
||||||
|
}, function(error) {
|
||||||
|
if (error) {
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEncryptedContent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
message.loadingBody = false;
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!senderPublicKey) {
|
||||||
// this should only happen if a mail from another channel is in the inbox
|
// this should only happen if a mail from another channel is in the inbox
|
||||||
email.body = 'Public key for sender not found!';
|
message.body = 'Public key for sender not found!';
|
||||||
localCallback(null, email);
|
message.decryptingBody = false;
|
||||||
|
callback(null, message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt and verfiy signatures
|
// get the receiver's public key to check the message signature
|
||||||
self._crypto.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) {
|
self._crypto.decrypt(message.body, senderPublicKey.publicKey, function(err, decrypted) {
|
||||||
if (err) {
|
// if an error occurs during decryption, display the error message as the message content
|
||||||
decrypted = err.errMsg;
|
decrypted = decrypted || err.errMsg || 'Error occurred during decryption';
|
||||||
}
|
|
||||||
|
|
||||||
// set encrypted flag
|
// this is a very primitive detection if we have PGP/MIME or PGP/INLINE
|
||||||
email.encrypted = true;
|
if (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && decrypted.indexOf('Content-Type:') === -1) {
|
||||||
|
message.body = decrypted;
|
||||||
// does our message block even need to be parsed?
|
message.decrypted = true;
|
||||||
// this is a very primitive detection if we have a mime node or plain text
|
message.decryptingBody = false;
|
||||||
// taking this out breaks compatibility to clients < 0.5
|
callback(null, message);
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse decrypted message
|
// parse the decrypted MIME message
|
||||||
self._imapParseMessageBlock({
|
self._imapParseMessageBlock({
|
||||||
message: email,
|
message: message,
|
||||||
block: decrypted
|
block: decrypted
|
||||||
}, function(error, parsedMessage) {
|
}, function(error) {
|
||||||
if (!parsedMessage) {
|
if (error) {
|
||||||
localCallback(error);
|
message.decryptingBody = false;
|
||||||
|
callback(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message.decrypted = true;
|
||||||
|
|
||||||
// remove the pgp-signature from the attachments
|
// remove the pgp-signature from the attachments
|
||||||
parsedMessage.attachments = _.reject(parsedMessage.attachments, function(attmt) {
|
message.attachments = _.reject(message.attachments, function(attmt) {
|
||||||
return attmt.mimeType === "application/pgp-signature";
|
return attmt.mimeType === "application/pgp-signature";
|
||||||
});
|
});
|
||||||
localCallback(error, parsedMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function extractArmoredContent(email) {
|
// we're done here!
|
||||||
var start = email.body.indexOf(str.cryptPrefix),
|
message.decryptingBody = false;
|
||||||
end = email.body.indexOf(str.cryptSuffix) + str.cryptSuffix.length;
|
callback(null, message);
|
||||||
|
});
|
||||||
// parse email body for encrypted message block
|
});
|
||||||
email.body = email.body.substring(start, end);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EmailDAO.prototype._imapMark = function(options, callback) {
|
EmailDAO.prototype._imapMark = function(options, callback) {
|
||||||
@ -1099,10 +1151,13 @@ define(function(require) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an email messsage including the email body from imap
|
* Get an email messsage without the body
|
||||||
* @param {String} options.messageId The
|
* @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;
|
var self = this;
|
||||||
|
|
||||||
if (!this._account.online) {
|
if (!this._account.online) {
|
||||||
@ -1113,17 +1168,34 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self._imapClient.getMessage({
|
self._imapClient.listMessagesByUid({
|
||||||
path: options.folder,
|
path: options.folder,
|
||||||
uid: options.uid
|
firstUid: options.firstUid,
|
||||||
}, function(err, message) {
|
lastUid: options.lastUid
|
||||||
if (err) {
|
}, callback);
|
||||||
callback(err);
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, message);
|
self._imapClient.getBody({
|
||||||
});
|
path: options.folder,
|
||||||
|
message: options.message
|
||||||
|
}, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
4
src/lib/iscroll/iscroll-min.js
vendored
4
src/lib/iscroll/iscroll-min.js
vendored
File diff suppressed because one or more lines are too long
@ -85,7 +85,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
div {
|
.line {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
&.empty-line {
|
&.empty-line {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<div class="list-wrapper" ng-iscroll="state.nav.currentFolder.messages.length">
|
<div class="list-wrapper" ng-iscroll="state.nav.currentFolder.messages.length">
|
||||||
<ul class="mail-list">
|
<ul class="mail-list">
|
||||||
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread, 'mail-list-replied': !email.unread && email.answered}" ng-click="select(email)" ng-repeat="email in state.nav.currentFolder.messages | orderBy:'uid':true | filter:searchText">
|
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread, 'mail-list-replied': !email.unread && email.answered}" ng-click="select(email)" ng-repeat="email in (filteredMessages = (state.nav.currentFolder.messages | orderBy:'uid':true | filter:searchText))">
|
||||||
<h3>{{email.from[0].name || email.from[0].address}}</h3>
|
<h3>{{email.from[0].name || email.from[0].address}}</h3>
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="flag" data-icon="{{(!email.unread && email.answered) ? '' : ''}}" ng-click="toggleUnread(email); $event.stopPropagation()"></div>
|
<div class="flag" data-icon="{{(!email.unread && email.answered) ? '' : ''}}" ng-click="toggleUnread(email); $event.stopPropagation()"></div>
|
||||||
|
@ -36,10 +36,13 @@
|
|||||||
</div><!--/.ng-switch-->
|
</div><!--/.ng-switch-->
|
||||||
|
|
||||||
|
|
||||||
<div class="body">
|
<div class="body" ng-switch="state.mailList.selected.encrypted === true && state.mailList.selected.decrypted === false">
|
||||||
|
<div class="line">
|
||||||
|
<div ng-switch-when="true">This message contains encrypted content.</div>
|
||||||
|
<div ng-switch-default>
|
||||||
<!-- Render lines of a text-email in divs for easier styling -->
|
<!-- Render lines of a text-email in divs for easier styling -->
|
||||||
<div ng-repeat="line in state.mailList.selected.body.split('\n') track by $index" ng-class="{'empty-line': lineEmpty(line)}">
|
<div ng-repeat="line in state.mailList.selected.body.split('\n') track by $index" ng-class="{'empty-line': lineEmpty(line)}">{{line}}<br></div>
|
||||||
{{line}}<br>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!--/.body-->
|
</div><!--/.body-->
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,8 @@ define(function(require) {
|
|||||||
KeychainDAO = require('js/dao/keychain-dao'),
|
KeychainDAO = require('js/dao/keychain-dao'),
|
||||||
appController = require('js/app-controller');
|
appController = require('js/app-controller');
|
||||||
|
|
||||||
|
chai.Assertion.includeStack = true;
|
||||||
|
|
||||||
describe('Mail List controller unit test', function() {
|
describe('Mail List controller unit test', function() {
|
||||||
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock,
|
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock,
|
||||||
emailAddress, notificationClickedHandler, emails,
|
emailAddress, notificationClickedHandler, emails,
|
||||||
@ -223,6 +225,77 @@ define(function(require) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getBody', function() {
|
||||||
|
it('should get the mail content', function() {
|
||||||
|
scope.state.nav = {
|
||||||
|
currentFolder: {
|
||||||
|
type: 'asd',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.getBody();
|
||||||
|
expect(emailDaoMock.getBody.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() {}
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
currentFolder: {
|
||||||
|
type: 'asd'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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() {
|
describe('remove', function() {
|
||||||
it('should not delete without a selected mail', function() {
|
it('should not delete without a selected mail', function() {
|
||||||
scope.remove();
|
scope.remove();
|
||||||
|
Loading…
Reference in New Issue
Block a user