mirror of
https://github.com/moparisthebest/mail
synced 2024-11-22 08:52:15 -05:00
[WO-895] Add paging and prefetching
This commit is contained in:
parent
88e83b6511
commit
d8fb06cb08
@ -62,7 +62,7 @@
|
|||||||
"grunt-svgmin": "~1.0.0",
|
"grunt-svgmin": "~1.0.0",
|
||||||
"grunt-svgstore": "~0.3.4",
|
"grunt-svgstore": "~0.3.4",
|
||||||
"iframe-resizer": "^2.8.3",
|
"iframe-resizer": "^2.8.3",
|
||||||
"imap-client": "~0.12.0",
|
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-895",
|
||||||
"jquery": "~2.1.1",
|
"jquery": "~2.1.1",
|
||||||
"mailbuild": "^0.3.7",
|
"mailbuild": "^0.3.7",
|
||||||
"mailreader": "~0.4.0",
|
"mailreader": "~0.4.0",
|
||||||
|
@ -54,24 +54,16 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
|||||||
// scope functions
|
// scope functions
|
||||||
//
|
//
|
||||||
|
|
||||||
$scope.getBody = function(message) {
|
$scope.getBody = function(messages) {
|
||||||
return $q(function(resolve) {
|
return $q(function(resolve) {
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return email.getBody({
|
return email.getBody({
|
||||||
folder: currentFolder(),
|
folder: currentFolder(),
|
||||||
message: message
|
messages: messages
|
||||||
});
|
});
|
||||||
|
|
||||||
}).then(function() {
|
|
||||||
// automatically decrypt if it's the selected message
|
|
||||||
if (message === currentMessage()) {
|
|
||||||
return email.decryptBody({
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
if (err.code !== 42) {
|
if (err.code !== 42) {
|
||||||
dialog.error(err);
|
dialog.error(err);
|
||||||
@ -136,6 +128,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
|||||||
* Date formatting
|
* Date formatting
|
||||||
*/
|
*/
|
||||||
$scope.formatDate = function(date) {
|
$scope.formatDate = function(date) {
|
||||||
|
if (!date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof date === 'string') {
|
if (typeof date === 'string') {
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
}
|
}
|
||||||
|
@ -112,9 +112,7 @@ var NavigationCtrl = function($scope, $location, $q, $timeout, account, email, o
|
|||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return email.refreshFolder({
|
return email.refreshOutbox();
|
||||||
folder: ob
|
|
||||||
});
|
|
||||||
|
|
||||||
}).catch(dialog.error);
|
}).catch(dialog.error);
|
||||||
};
|
};
|
||||||
|
@ -52,6 +52,12 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
|
|||||||
status.setReading(false);
|
status.setReading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.decrypt = function(message) {
|
||||||
|
return email.decryptBody({
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getKeyId = function(address) {
|
$scope.getKeyId = function(address) {
|
||||||
if ($location.search().dev || !address) {
|
if ($location.search().dev || !address) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var PREFETCH_ITEMS = 10;
|
||||||
|
|
||||||
var ngModule = angular.module('woDirectives');
|
var ngModule = angular.module('woDirectives');
|
||||||
|
|
||||||
ngModule.directive('listScroll', function($timeout) {
|
ngModule.directive('listScroll', function($timeout) {
|
||||||
@ -20,7 +22,10 @@ ngModule.directive('listScroll', function($timeout) {
|
|||||||
inViewport = false,
|
inViewport = false,
|
||||||
listItem, message,
|
listItem, message,
|
||||||
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible,
|
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible,
|
||||||
displayMessages = scope[model];
|
displayMessages = scope[model],
|
||||||
|
visible = [],
|
||||||
|
prefetchLowerBound = displayMessages.length, // lowest index where we need to start prefetching
|
||||||
|
prefetchUpperBound = 0; // highest index where we need to start prefetching
|
||||||
|
|
||||||
if (!top && !bottom) {
|
if (!top && !bottom) {
|
||||||
// list not visible
|
// list not visible
|
||||||
@ -38,7 +43,6 @@ ngModule.directive('listScroll', function($timeout) {
|
|||||||
}
|
}
|
||||||
message = displayMessages[i];
|
message = displayMessages[i];
|
||||||
|
|
||||||
|
|
||||||
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top
|
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
|
isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom
|
||||||
isVisible = (listItem.top || listItem.bottom) && listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
|
isVisible = (listItem.top || listItem.bottom) && listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
|
||||||
@ -47,12 +51,27 @@ ngModule.directive('listScroll', function($timeout) {
|
|||||||
// we are now iterating over visible elements
|
// we are now iterating over visible elements
|
||||||
inViewport = true;
|
inViewport = true;
|
||||||
// load mail body of visible
|
// load mail body of visible
|
||||||
scope.getBody(message);
|
visible.push(message);
|
||||||
|
|
||||||
|
prefetchLowerBound = Math.max(Math.min(prefetchLowerBound, i - 1), 0);
|
||||||
|
prefetchUpperBound = Math.max(prefetchUpperBound, i + 1);
|
||||||
} else if (inViewport) {
|
} else if (inViewport) {
|
||||||
// we are leaving the viewport, so stop iterating over the items
|
// we are leaving the viewport, so stop iterating over the items
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// prefetch: [prefetchLowerBound - 20 ; prefetchLowerBound] and [prefetchUpperBound; prefetchUpperBound + 20]
|
||||||
|
//
|
||||||
|
|
||||||
|
// normalize lowest index to 0, slice interprets values <0 as "start from end"
|
||||||
|
var prefetchLower = displayMessages.slice(Math.max(prefetchLowerBound - PREFETCH_ITEMS, 0), prefetchLowerBound);
|
||||||
|
var prefetchUpper = displayMessages.slice(prefetchUpperBound, prefetchUpperBound + PREFETCH_ITEMS);
|
||||||
|
|
||||||
|
visible.concat(prefetchLower).concat(prefetchUpper).forEach(function(email) {
|
||||||
|
scope.getBody([email]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.loadVisibleBodies = function() {
|
scope.loadVisibleBodies = function() {
|
||||||
|
@ -92,7 +92,14 @@ ngModule.directive('frameLoad', function($window) {
|
|||||||
|
|
||||||
function displayText(body) {
|
function displayText(body) {
|
||||||
var mail = scope.state.mailList.selected;
|
var mail = scope.state.mailList.selected;
|
||||||
if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) {
|
|
||||||
|
if (mail && mail.encrypted && !mail.decrypted) {
|
||||||
|
// decrypt current mail
|
||||||
|
scope.decrypt(mail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mail && mail.html) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ var FOLDER_TYPE_TRASH = 'Trash';
|
|||||||
var FOLDER_TYPE_FLAGGED = 'Flagged';
|
var FOLDER_TYPE_FLAGGED = 'Flagged';
|
||||||
|
|
||||||
var MSG_ATTR_UID = 'uid';
|
var MSG_ATTR_UID = 'uid';
|
||||||
|
var MSG_ATTR_MODSEQ = 'modseq';
|
||||||
var MSG_PART_ATTR_CONTENT = 'content';
|
var MSG_PART_ATTR_CONTENT = 'content';
|
||||||
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
|
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
|
||||||
var MSG_PART_TYPE_ENCRYPTED = 'encrypted';
|
var MSG_PART_TYPE_ENCRYPTED = 'encrypted';
|
||||||
@ -83,13 +84,18 @@ function Email(keychain, pgp, accountStore, pgpbuilder, mailreader, dialog, appC
|
|||||||
* @resolve {Object} keypair
|
* @resolve {Object} keypair
|
||||||
*/
|
*/
|
||||||
Email.prototype.init = function(options) {
|
Email.prototype.init = function(options) {
|
||||||
this._account = options.account;
|
var self = this;
|
||||||
this._account.busy = 0; // > 0 triggers the spinner
|
|
||||||
this._account.online = false;
|
|
||||||
this._account.loggingIn = false;
|
|
||||||
|
|
||||||
// init folders from memory
|
self._account = options.account;
|
||||||
return this._initFoldersFromDisk();
|
self._account.busy = 0; // >0 triggers the spinner
|
||||||
|
self._account.online = false;
|
||||||
|
self._account.loggingIn = false;
|
||||||
|
|
||||||
|
// fetch folders from idb
|
||||||
|
return self._devicestorage.listItems(FOLDER_DB_TYPE, true).then(function(stored) {
|
||||||
|
self._account.folders = stored[0] || [];
|
||||||
|
return self._initFolders();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -202,117 +208,6 @@ Email.prototype.openFolder = function(options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Synchronizes a folder's contents from disk to memory, i.e. if
|
|
||||||
* a message has disappeared from the disk, this method will remove it from folder.messages, and
|
|
||||||
* it adds any messages from disk to memory the are not yet in folder.messages
|
|
||||||
*
|
|
||||||
* @param {Object} options.folder The folder to synchronize
|
|
||||||
*/
|
|
||||||
Email.prototype.refreshFolder = function(options) {
|
|
||||||
var self = this,
|
|
||||||
folder = options.folder;
|
|
||||||
|
|
||||||
self.busy();
|
|
||||||
folder.messages = folder.messages || [];
|
|
||||||
|
|
||||||
return self._localListMessages({
|
|
||||||
folder: folder
|
|
||||||
}).then(function(storedMessages) {
|
|
||||||
var storedUids = _.pluck(storedMessages, MSG_ATTR_UID),
|
|
||||||
memoryUids = _.pluck(folder.messages, MSG_ATTR_UID),
|
|
||||||
newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory
|
|
||||||
removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk
|
|
||||||
|
|
||||||
// which messages are new on the disk that are not yet in memory?
|
|
||||||
_.filter(storedMessages, function(msg) {
|
|
||||||
return _.contains(newUids, msg.uid);
|
|
||||||
}).forEach(function(newMessage) {
|
|
||||||
// remove the body parts to not load unnecessary data to memory
|
|
||||||
// however, don't do that for the outbox. load the full message there.
|
|
||||||
if (folder.path !== config.outboxMailboxPath) {
|
|
||||||
delete newMessage.bodyParts;
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.messages.push(newMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
// which messages are no longer on disk, i.e. have been removed/sent/...
|
|
||||||
_.filter(folder.messages, function(msg) {
|
|
||||||
return _.contains(removedUids, msg.uid);
|
|
||||||
}).forEach(function(removedMessage) {
|
|
||||||
// remove the message
|
|
||||||
var index = folder.messages.indexOf(removedMessage);
|
|
||||||
folder.messages.splice(index, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
}).then(done).catch(done);
|
|
||||||
|
|
||||||
function done(err) {
|
|
||||||
self.done(); // stop the spinner
|
|
||||||
updateUnreadCount(folder); // update the unread count
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a message's headers from IMAP.
|
|
||||||
*
|
|
||||||
* @param {Object} options.folder The folder for which to fetch the message
|
|
||||||
*/
|
|
||||||
Email.prototype.fetchMessages = function(options) {
|
|
||||||
var self = this,
|
|
||||||
folder = options.folder;
|
|
||||||
|
|
||||||
self.busy();
|
|
||||||
|
|
||||||
return new Promise(function(resolve) {
|
|
||||||
self.checkOnline();
|
|
||||||
resolve();
|
|
||||||
|
|
||||||
}).then(function() {
|
|
||||||
// list the messages starting from the lowest new uid to the highest new uid
|
|
||||||
return self._imapListMessages(options);
|
|
||||||
|
|
||||||
}).then(function(messages) {
|
|
||||||
if (_.isEmpty(messages)) {
|
|
||||||
// nothing to do, we're done here
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// persist the messages to the local storage
|
|
||||||
return self._localStoreMessages({
|
|
||||||
folder: folder,
|
|
||||||
emails: messages
|
|
||||||
}).then(function() {
|
|
||||||
// show the attachment clip in the message list ui
|
|
||||||
messages.forEach(function(message) {
|
|
||||||
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
|
||||||
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
[].unshift.apply(folder.messages, messages); // add the new messages to the folder
|
|
||||||
updateUnreadCount(folder); // update the unread count
|
|
||||||
|
|
||||||
// notify about new messages only for the inbox
|
|
||||||
if (folder.type === FOLDER_TYPE_INBOX) {
|
|
||||||
self.onIncomingMessage(messages);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}).then(done).catch(done);
|
|
||||||
|
|
||||||
function done(err) {
|
|
||||||
self.done(); // stop the spinner
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a message from IMAP, disk and folder.messages.
|
* Delete a message from IMAP, disk and folder.messages.
|
||||||
@ -520,173 +415,112 @@ Email.prototype.moveMessage = function(options) {
|
|||||||
*/
|
*/
|
||||||
Email.prototype.getBody = function(options) {
|
Email.prototype.getBody = function(options) {
|
||||||
var self = this,
|
var self = this,
|
||||||
message = options.message,
|
messages = options.messages,
|
||||||
folder = options.folder,
|
folder = options.folder;
|
||||||
localMessage, attachmentParts;
|
|
||||||
|
|
||||||
|
messages = messages.filter(function(message) {
|
||||||
// the message either already has a body or is fetching it right now, so no need to become active here
|
// the message either already has a body or is fetching it right now, so no need to become active here
|
||||||
if (message.loadingBody || typeof message.body !== 'undefined') {
|
return !(message.loadingBody || typeof message.body !== 'undefined');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messages.length) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messages.forEach(function(message) {
|
||||||
message.loadingBody = true;
|
message.loadingBody = true;
|
||||||
|
});
|
||||||
|
|
||||||
self.busy();
|
self.busy();
|
||||||
|
|
||||||
/*
|
var loadedMessages;
|
||||||
* read this before inspecting the method!
|
|
||||||
*
|
|
||||||
* you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior:
|
|
||||||
* 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array,
|
|
||||||
* because it is not really necessary to keep everything in memory
|
|
||||||
* 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property
|
|
||||||
* to the model. this property is auto generated and must not be persisted.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// load the local message from memory
|
// load the message from disk
|
||||||
return self._localListMessages({
|
return self._localListMessages({
|
||||||
folder: folder,
|
folder: folder,
|
||||||
uid: message.uid
|
uid: _.pluck(messages, MSG_ATTR_UID)
|
||||||
}).then(function(localMessages) {
|
}).then(function(localMessages) {
|
||||||
localMessage = localMessages[0];
|
loadedMessages = localMessages;
|
||||||
|
|
||||||
if (!localMessage) {
|
// find out which messages are not available on disk (uids not included in disk roundtrip)
|
||||||
// the message has been deleted in the meantime
|
var localUids = _.pluck(localMessages, MSG_ATTR_UID);
|
||||||
var error = new Error('Can not get the contents of this message. It has already been deleted!');
|
var needsImapFetch = messages.filter(function(msg) {
|
||||||
error.hide = true;
|
return !_.contains(localUids, msg.uid);
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// treat attachment and non-attachment body parts separately:
|
|
||||||
// we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html)
|
|
||||||
// but we spare the effort and fetch attachment content later upon explicit user request.
|
|
||||||
var contentParts = localMessage.bodyParts.filter(function(bodyPart) {
|
|
||||||
return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id);
|
|
||||||
});
|
|
||||||
attachmentParts = localMessage.bodyParts.filter(function(bodyPart) {
|
|
||||||
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id;
|
|
||||||
});
|
});
|
||||||
|
return needsImapFetch;
|
||||||
|
|
||||||
// do we need to fetch content from the imap server?
|
}).then(function(needsImapFetch) {
|
||||||
var needsFetch = false;
|
// get the missing messages from imap
|
||||||
contentParts.forEach(function(part) {
|
|
||||||
needsFetch = (typeof part.content === 'undefined');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!needsFetch) {
|
if (!needsImapFetch.length) {
|
||||||
// if we have all the content we need,
|
// no imap roundtrip needed, we're done
|
||||||
// we can extract the content
|
return loadedMessages;
|
||||||
message.bodyParts = localMessage.bodyParts;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the raw content from the imap server
|
// do the imap roundtrip
|
||||||
return self._getBodyParts({
|
return self._fetchMessages({
|
||||||
folder: folder,
|
messages: needsImapFetch,
|
||||||
uid: localMessage.uid,
|
folder: folder
|
||||||
bodyParts: contentParts
|
}).then(function(imapMessages) {
|
||||||
}).then(function(parsedBodyParts) {
|
// add the messages from imap to the loaded messages
|
||||||
// piece together the parsed bodyparts and the empty attachments which have not been parsed
|
loadedMessages = loadedMessages.concat(imapMessages);
|
||||||
message.bodyParts = parsedBodyParts.concat(attachmentParts);
|
|
||||||
localMessage.bodyParts = parsedBodyParts.concat(attachmentParts);
|
|
||||||
|
|
||||||
// persist it to disk
|
|
||||||
return self._localStoreMessages({
|
|
||||||
folder: folder,
|
|
||||||
emails: [localMessage]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}).then(function() {
|
|
||||||
// extract the content
|
|
||||||
if (message.encrypted) {
|
|
||||||
// show the encrypted message
|
|
||||||
message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var root = message.bodyParts;
|
|
||||||
|
|
||||||
if (message.signed) {
|
|
||||||
// PGP/MIME signed
|
|
||||||
var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest
|
|
||||||
message.signedMessage = signedRoot.signedMessage;
|
|
||||||
message.signature = signedRoot.signature;
|
|
||||||
root = signedRoot.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n');
|
|
||||||
|
|
||||||
/*
|
|
||||||
* if the message is plain text and contains pgp/inline, we are only interested in the encrypted
|
|
||||||
* content, the rest (corporate mail footer, attachments, etc.) is discarded.
|
|
||||||
* "-----BEGIN/END (...)-----" must be at the start/end of a line,
|
|
||||||
* the regex must not match a pgp block in a plain text reply or forward of a pgp/inline message,
|
|
||||||
* the encryption will break for replies/forward, because "> " corrupts the PGP block with non-radix-64 characters,
|
|
||||||
*/
|
|
||||||
var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im.exec(body);
|
|
||||||
if (pgpInlineMatch) {
|
|
||||||
message.body = pgpInlineMatch[0]; // show the plain text content
|
|
||||||
message.encrypted = true; // signal the ui that we're handling encrypted content
|
|
||||||
|
|
||||||
// replace the bodyParts info with an artificial bodyPart of type "encrypted"
|
|
||||||
message.bodyParts = [{
|
|
||||||
type: MSG_PART_TYPE_ENCRYPTED,
|
|
||||||
content: pgpInlineMatch[0],
|
|
||||||
_isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader
|
|
||||||
}];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* any content before/after the PGP block will be discarded,
|
|
||||||
* "-----BEGIN/END (...)-----" must be at the start/end of a line,
|
|
||||||
* after the hash (and possibly other) arbitrary headers, the signed payload begins,
|
|
||||||
* the text is followed by a final \n and then the pgp signature begins
|
|
||||||
* untrusted attachments and html is ignored
|
|
||||||
*/
|
|
||||||
var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body);
|
|
||||||
if (clearSignedMatch) {
|
|
||||||
// PGP/INLINE signed
|
|
||||||
message.signed = true;
|
|
||||||
message.clearSignedMessage = clearSignedMatch[0];
|
|
||||||
body = clearSignedMatch[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.signed) {
|
|
||||||
// message is not signed, so we're done here
|
|
||||||
return setBody(body, root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the signatures for signed messages
|
|
||||||
return self._checkSignatures(message).then(function(signaturesValid) {
|
|
||||||
message.signaturesValid = signaturesValid;
|
|
||||||
setBody(body, root);
|
|
||||||
});
|
|
||||||
|
|
||||||
}).then(function() {
|
|
||||||
self.done();
|
|
||||||
message.loadingBody = false;
|
|
||||||
return message;
|
|
||||||
|
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
self.done();
|
axe.error('Can not fetch messages from IMAP. Reason: ' + err.message + (err.stack ? ('\n' + err.stack) : ''));
|
||||||
|
|
||||||
|
// stop the loading spinner for those messages we can't fetch
|
||||||
|
needsImapFetch.forEach(function(message) {
|
||||||
message.loadingBody = false;
|
message.loadingBody = false;
|
||||||
if (err.hide) {
|
});
|
||||||
// ignore errors with err.hide
|
|
||||||
return message;
|
// we can't fetch from imap, just continue with what we have
|
||||||
|
messages = _.difference(messages, needsImapFetch);
|
||||||
|
});
|
||||||
|
|
||||||
|
}).then(function() {
|
||||||
|
// enhance dummy messages with content
|
||||||
|
messages.forEach(function(message) {
|
||||||
|
var loadedMessage = _.findWhere(loadedMessages, {
|
||||||
|
uid: message.uid
|
||||||
|
});
|
||||||
|
|
||||||
|
// enhance the dummy message with the loaded content
|
||||||
|
_.extend(message, loadedMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
}).then(function() {
|
||||||
|
// extract the message body
|
||||||
|
var jobs = [];
|
||||||
|
|
||||||
|
messages.forEach(function(message) {
|
||||||
|
var job = self._extractBody(message).catch(function(err) {
|
||||||
|
axe.error('Can extract body for message uid ' + message.uid + ' . Reason: ' + err.message + (err.stack ? ('\n' + err.stack) : ''));
|
||||||
|
});
|
||||||
|
jobs.push(job);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(jobs);
|
||||||
|
}).then(function() {
|
||||||
|
done();
|
||||||
|
|
||||||
|
if (options.notifyNew && messages.length) {
|
||||||
|
// notify for incoming mail
|
||||||
|
self.onIncomingMessage(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}).catch(function(err) {
|
||||||
|
done();
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
function setBody(body, root) {
|
function done() {
|
||||||
message.body = body;
|
messages.forEach(function(message) {
|
||||||
if (!message.clearSignedMessage) {
|
message.loadingBody = false;
|
||||||
message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT);
|
});
|
||||||
message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n');
|
self.done();
|
||||||
inlineExternalImages(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -939,6 +773,47 @@ Email.prototype.encrypt = function(options) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes the outbox's contents from disk to memory.
|
||||||
|
* If a message has disappeared from the disk, this method will remove
|
||||||
|
* it from folder.messages, and it adds any messages from disk to memory the are not yet in folder.messages
|
||||||
|
*
|
||||||
|
* @param {Object} options.folder The folder to synchronize
|
||||||
|
*/
|
||||||
|
Email.prototype.refreshOutbox = function() {
|
||||||
|
var outbox = _.findWhere(this._account.folders, {
|
||||||
|
type: config.outboxMailboxType
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._localListMessages({
|
||||||
|
folder: outbox,
|
||||||
|
exactmatch: false
|
||||||
|
}).then(function(storedMessages) {
|
||||||
|
var storedUids = _.pluck(storedMessages, MSG_ATTR_UID),
|
||||||
|
memoryUids = _.pluck(outbox.messages, MSG_ATTR_UID),
|
||||||
|
newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory
|
||||||
|
removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk
|
||||||
|
|
||||||
|
// add new messages that are not yet in memory
|
||||||
|
_.filter(storedMessages, function(msg) {
|
||||||
|
return _.contains(newUids, msg.uid);
|
||||||
|
}).forEach(function(newMessage) {
|
||||||
|
outbox.messages.push(newMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove messages that are no longer on disk, i.e. have been removed/sent/...
|
||||||
|
_.filter(outbox.messages, function(msg) {
|
||||||
|
return _.contains(removedUids, msg.uid);
|
||||||
|
}).forEach(function(removedMessage) {
|
||||||
|
var index = outbox.messages.indexOf(removedMessage);
|
||||||
|
outbox.messages.splice(index, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUnreadCount(outbox); // update the unread count
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
@ -987,7 +862,7 @@ Email.prototype.onConnect = function(imap) {
|
|||||||
}).then(function() {
|
}).then(function() {
|
||||||
self._account.loggingIn = false;
|
self._account.loggingIn = false;
|
||||||
// init folders
|
// init folders
|
||||||
return self._initFoldersFromImap();
|
return self._updateFolders();
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
// fill the imap mailboxCache with information we have locally available:
|
// fill the imap mailboxCache with information we have locally available:
|
||||||
@ -997,31 +872,17 @@ Email.prototype.onConnect = function(imap) {
|
|||||||
// - next expected uid
|
// - next expected uid
|
||||||
var mailboxCache = {};
|
var mailboxCache = {};
|
||||||
self._account.folders.forEach(function(folder) {
|
self._account.folders.forEach(function(folder) {
|
||||||
if (folder.messages.length === 0) {
|
var uids = folder.uids.sort(function(a, b) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var uids, highestModseq, lastUid;
|
|
||||||
|
|
||||||
uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) {
|
|
||||||
return a - b;
|
return a - b;
|
||||||
});
|
});
|
||||||
lastUid = uids[uids.length - 1];
|
var lastUid = uids[uids.length - 1];
|
||||||
|
|
||||||
highestModseq = (_.pluck(folder.messages, 'modseq').sort(function(a, b) {
|
|
||||||
// We treat modseq values as numbers here as an exception, should
|
|
||||||
// be strings everywhere else.
|
|
||||||
// If it turns out that someone actually uses 64 bit uint numbers
|
|
||||||
// that do not fit to the JavaScript number type then we should
|
|
||||||
// use a helper for handling big integers.
|
|
||||||
return (Number(a) || 0) - (Number(b) || 0);
|
|
||||||
}).pop() || 0).toString();
|
|
||||||
|
|
||||||
mailboxCache[folder.path] = {
|
mailboxCache[folder.path] = {
|
||||||
exists: lastUid,
|
exists: lastUid,
|
||||||
uidNext: lastUid + 1,
|
uidNext: lastUid + 1,
|
||||||
uidlist: uids,
|
uidlist: uids,
|
||||||
highestModseq: highestModseq
|
highestModseq: '' + folder.modseq
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
self._imapClient.mailboxCache = mailboxCache;
|
self._imapClient.mailboxCache = mailboxCache;
|
||||||
@ -1099,27 +960,54 @@ Email.prototype.onDisconnect = function() {
|
|||||||
* @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively
|
* @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively
|
||||||
*/
|
*/
|
||||||
Email.prototype._onSyncUpdate = function(options) {
|
Email.prototype._onSyncUpdate = function(options) {
|
||||||
var self = this;
|
var self = this,
|
||||||
|
uids = options.list;
|
||||||
|
|
||||||
var folder = _.findWhere(self._account.folders, {
|
var folder = _.findWhere(self._account.folders, {
|
||||||
path: options.path
|
path: options.path
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
// ignore updates for an unknown folder
|
return; // ignore updates for an unknown folder
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.type === SYNC_TYPE_NEW) {
|
if (options.type === SYNC_TYPE_NEW) {
|
||||||
// new messages available on imap, fetch from imap and store to disk and memory
|
// new messages available on imap, add the new uids to the folder
|
||||||
self.fetchMessages({
|
|
||||||
|
uids = _.difference(uids, folder.uids); // eliminate duplicates
|
||||||
|
var maxUid = folder.uids.length ? Math.max.apply(null, folder.uids) : 0; // find highest uid prior to update
|
||||||
|
|
||||||
|
// add to folder's uids, persist folder
|
||||||
|
Array.prototype.push.apply(folder.uids, uids);
|
||||||
|
self._localStoreFolders();
|
||||||
|
|
||||||
|
// add dummy messages to the message list
|
||||||
|
Array.prototype.push.apply(folder.messages, uids.map(function(uid) {
|
||||||
|
return {
|
||||||
|
uid: uid
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (maxUid) {
|
||||||
|
// folder not empty, find and download the 20 newest bodies. Notify for the inbox
|
||||||
|
var fetch = _.filter(folder.messages, function(msg) {
|
||||||
|
return msg.uid > maxUid;
|
||||||
|
}).sort(function(a, b) {
|
||||||
|
return a.uid - b.uid;
|
||||||
|
}).slice(-20);
|
||||||
|
|
||||||
|
self.getBody({
|
||||||
folder: folder,
|
folder: folder,
|
||||||
firstUid: Math.min.apply(null, options.list),
|
messages: fetch,
|
||||||
lastUid: Math.max.apply(null, options.list)
|
notifyNew: folder.type === FOLDER_TYPE_INBOX
|
||||||
}).then(self._dialog.error).catch(self._dialog.error);
|
}).catch(self._dialog.error);
|
||||||
|
}
|
||||||
|
|
||||||
} else if (options.type === SYNC_TYPE_DELETED) {
|
} else if (options.type === SYNC_TYPE_DELETED) {
|
||||||
// messages have been deleted, remove from local storage and memory
|
// messages have been deleted
|
||||||
options.list.forEach(function(uid) {
|
|
||||||
|
folder.uids = _.difference(folder.uids, uids); // remove the uids from the uid list
|
||||||
|
uids.forEach(function(uid) {
|
||||||
var message = _.findWhere(folder.messages, {
|
var message = _.findWhere(folder.messages, {
|
||||||
uid: uid
|
uid: uid
|
||||||
});
|
});
|
||||||
@ -1132,12 +1020,12 @@ Email.prototype._onSyncUpdate = function(options) {
|
|||||||
folder: folder,
|
folder: folder,
|
||||||
message: message,
|
message: message,
|
||||||
localOnly: true
|
localOnly: true
|
||||||
}).then(self._dialog.error).catch(self._dialog.error);
|
}).catch(self._dialog.error);
|
||||||
});
|
});
|
||||||
} else if (options.type === SYNC_TYPE_MSGS) {
|
} else if (options.type === SYNC_TYPE_MSGS) {
|
||||||
// NB! several possible reasons why this could be called.
|
// NB! several possible reasons why this could be called.
|
||||||
// if a message in the array has uid value and flag array, it had a possible flag update
|
// if a message in the array has uid value and flag array, it had a possible flag update
|
||||||
options.list.forEach(function(changedMsg) {
|
uids.forEach(function(changedMsg) {
|
||||||
if (!changedMsg.uid || !changedMsg.flags) {
|
if (!changedMsg.uid || !changedMsg.flags) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1146,7 +1034,7 @@ Email.prototype._onSyncUpdate = function(options) {
|
|||||||
uid: changedMsg.uid
|
uid: changedMsg.uid
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!message) {
|
if (!message || !message.bodyParts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1159,7 +1047,14 @@ Email.prototype._onSyncUpdate = function(options) {
|
|||||||
folder: folder,
|
folder: folder,
|
||||||
message: message,
|
message: message,
|
||||||
localOnly: true
|
localOnly: true
|
||||||
}).then(self._dialog.error).catch(self._dialog.error);
|
}).then(function() {
|
||||||
|
// update the folder's last known modseq if necessary
|
||||||
|
var modseq = parseInt(changedMsg.modseq, 10);
|
||||||
|
if (modseq > folder.modseq) {
|
||||||
|
folder.modseq = modseq;
|
||||||
|
return self._localStoreFolders();
|
||||||
|
}
|
||||||
|
}).catch(self._dialog.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1172,36 +1067,12 @@ Email.prototype._onSyncUpdate = function(options) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the folder information from memory, and adds/removes folders in account.folders.
|
|
||||||
* The locally available messages are loaded from memory
|
|
||||||
*/
|
|
||||||
Email.prototype._initFoldersFromDisk = function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.busy(); // start the spinner
|
|
||||||
|
|
||||||
// fetch list from local cache
|
|
||||||
return self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null).then(function(stored) {
|
|
||||||
self._account.folders = stored[0] || [];
|
|
||||||
return self._initMessagesFromDisk();
|
|
||||||
|
|
||||||
}).then(done).catch(done);
|
|
||||||
|
|
||||||
function done(err) {
|
|
||||||
self.done(); // stop the spinner
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the folder information from imap (if we're online). Adds/removes folders in account.folders,
|
* Updates the folder information from imap (if we're online). Adds/removes folders in account.folders,
|
||||||
* if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages,
|
* if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages,
|
||||||
* all the locally available messages are loaded from memory.
|
* all the locally available messages are loaded from memory.
|
||||||
*/
|
*/
|
||||||
Email.prototype._initFoldersFromImap = function() {
|
Email.prototype._updateFolders = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.busy(); // start the spinner
|
self.busy(); // start the spinner
|
||||||
@ -1313,25 +1184,12 @@ Email.prototype._initFoldersFromImap = function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// if folders have not changed, can fill them with messages directly
|
// if folders have not changed, can fill them with messages directly
|
||||||
if (!foldersChanged) {
|
if (foldersChanged) {
|
||||||
return;
|
return self._localStoreFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist encrypted list in device storage
|
|
||||||
// note: the folders in the ui also include the messages array, so let's create a clean array here
|
|
||||||
var folders = self._account.folders.map(function(folder) {
|
|
||||||
return {
|
|
||||||
name: folder.name,
|
|
||||||
path: folder.path,
|
|
||||||
type: folder.type,
|
|
||||||
wellknown: !!folder.wellknown
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return self._devicestorage.storeList([folders], FOLDER_DB_TYPE);
|
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return self._initMessagesFromDisk();
|
return self._initFolders();
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
self.done();
|
self.done();
|
||||||
@ -1342,28 +1200,32 @@ Email.prototype._initFoldersFromImap = function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
Email.prototype._initFolders = function() {
|
||||||
* Fill uninitialized folders with the locally available messages.
|
|
||||||
*/
|
|
||||||
Email.prototype._initMessagesFromDisk = function() {
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var jobs = [];
|
|
||||||
self._account.folders.forEach(function(folder) {
|
self._account.folders.forEach(function(folder) {
|
||||||
if (folder.messages) {
|
folder.modseq = folder.modseq || 0;
|
||||||
// the folder is already initialized
|
folder.uids = folder.uids || []; // attach an empty uids array to the folder
|
||||||
return;
|
folder.uids.sort(function(a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
folder.messages = folder.messages || folder.uids.map(function(uid) {
|
||||||
|
// fill the messages array with dummy messages, messages will be fetched later
|
||||||
|
return {
|
||||||
|
uid: uid
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var inbox = _.findWhere(self._account.folders, {
|
||||||
|
type: FOLDER_TYPE_INBOX
|
||||||
|
});
|
||||||
|
if (inbox && inbox.messages.length) {
|
||||||
|
return self.getBody({
|
||||||
|
folder: inbox,
|
||||||
|
messages: inbox.messages.slice(-30)
|
||||||
|
}).catch(self._dialog.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync messages from disk to the folder model
|
|
||||||
jobs.push(self.refreshFolder({
|
|
||||||
folder: folder
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(jobs).then(function() {
|
|
||||||
return; // don't return promise array
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Email.prototype.busy = function() {
|
Email.prototype.busy = function() {
|
||||||
@ -1461,27 +1323,6 @@ Email.prototype._imapMoveMessage = function(options) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list messsage headers 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)
|
|
||||||
* @return {Promise}
|
|
||||||
* @resolve {Array} messages The message meta data
|
|
||||||
*/
|
|
||||||
Email.prototype._imapListMessages = function(options) {
|
|
||||||
var self = this;
|
|
||||||
return new Promise(function(resolve) {
|
|
||||||
self.checkOnline();
|
|
||||||
resolve();
|
|
||||||
}).then(function() {
|
|
||||||
options.path = options.folder.path;
|
|
||||||
return self._imapClient.listMessages(options);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a built message to a folder
|
* Uploads a built message to a folder
|
||||||
*
|
*
|
||||||
@ -1497,11 +1338,98 @@ Email.prototype._imapUploadMessage = function(options) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch messages from imap
|
||||||
|
*/
|
||||||
|
Email.prototype._fetchMessages = function(options) {
|
||||||
|
var self = this,
|
||||||
|
messages = options.messages,
|
||||||
|
folder = options.folder;
|
||||||
|
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
self.checkOnline();
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
}).then(function() {
|
||||||
|
// fetch all the metadata at once
|
||||||
|
return self._imapClient.listMessages({
|
||||||
|
path: folder.path,
|
||||||
|
uids: _.pluck(messages, MSG_ATTR_UID)
|
||||||
|
});
|
||||||
|
|
||||||
|
}).then(function(msgs) {
|
||||||
|
messages = msgs;
|
||||||
|
// displays the clip in the UI if the message contains attachments
|
||||||
|
messages.forEach(function(message) {
|
||||||
|
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
||||||
|
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the bodies from imap (individual roundtrips per msg)
|
||||||
|
var jobs = [];
|
||||||
|
|
||||||
|
messages.forEach(function(message) {
|
||||||
|
// fetch only the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html)
|
||||||
|
var contentParts = message.bodyParts.filter(function(bodyPart) {
|
||||||
|
return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id);
|
||||||
|
});
|
||||||
|
var attachmentParts = message.bodyParts.filter(function(bodyPart) {
|
||||||
|
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!contentParts.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the imap roundtrip
|
||||||
|
var job = self._getBodyParts({
|
||||||
|
folder: folder,
|
||||||
|
uid: message.uid,
|
||||||
|
bodyParts: contentParts
|
||||||
|
}).then(function(parsedBodyParts) {
|
||||||
|
// concat parsed bodyparts and the empty attachment parts
|
||||||
|
message.bodyParts = parsedBodyParts.concat(attachmentParts);
|
||||||
|
|
||||||
|
// store fetched message
|
||||||
|
return self._localStoreMessages({
|
||||||
|
folder: folder,
|
||||||
|
emails: [message]
|
||||||
|
});
|
||||||
|
}).catch(function(err) {
|
||||||
|
// ignore errors with err.hide, throw otherwise
|
||||||
|
if (err.hide) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jobs.push(job);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(jobs);
|
||||||
|
}).then(function() {
|
||||||
|
// update the folder's last known modseq if necessary
|
||||||
|
var highestModseq = Math.max.apply(null, _.pluck(messages, MSG_ATTR_MODSEQ).map(function(modseq) {
|
||||||
|
return parseInt(modseq, 10);
|
||||||
|
}));
|
||||||
|
if (highestModseq > folder.modseq) {
|
||||||
|
folder.modseq = highestModseq;
|
||||||
|
return self._localStoreFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then(function() {
|
||||||
|
updateUnreadCount(folder); // update the unread count
|
||||||
|
return messages;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream an email messsage's body
|
* Stream an email messsage's body
|
||||||
* @param {String} options.folder The folder
|
* @param {String} options.folder The folder
|
||||||
* @param {String} options.uid the message's uid
|
* @param {String} options.uid the message's uid
|
||||||
* @param {Object} options.bodyParts The message, as retrieved by _imapListMessages
|
* @param {Object} options.bodyParts The message parts
|
||||||
*/
|
*/
|
||||||
Email.prototype._getBodyParts = function(options) {
|
Email.prototype._getBodyParts = function(options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@ -1531,6 +1459,24 @@ Email.prototype._getBodyParts = function(options) {
|
|||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* persist encrypted list in device storage
|
||||||
|
* note: the folders in the ui also include the messages array, so let's create a clean array here
|
||||||
|
*/
|
||||||
|
Email.prototype._localStoreFolders = function() {
|
||||||
|
var folders = this._account.folders.map(function(folder) {
|
||||||
|
return {
|
||||||
|
name: folder.name,
|
||||||
|
path: folder.path,
|
||||||
|
type: folder.type,
|
||||||
|
modseq: folder.modseq,
|
||||||
|
wellknown: !!folder.wellknown,
|
||||||
|
uids: folder.uids
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._devicestorage.storeList([folders], FOLDER_DB_TYPE);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided),
|
* List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided),
|
||||||
@ -1540,8 +1486,21 @@ Email.prototype._getBodyParts = function(options) {
|
|||||||
* @param {Object} options.uid A specific uid to look up locally in the folder
|
* @param {Object} options.uid A specific uid to look up locally in the folder
|
||||||
*/
|
*/
|
||||||
Email.prototype._localListMessages = function(options) {
|
Email.prototype._localListMessages = function(options) {
|
||||||
var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : '');
|
var query;
|
||||||
return this._devicestorage.listItems(dbType, 0, null);
|
|
||||||
|
var needsExactMatch = typeof options.exactmatch === 'undefined' ? true : options.exactmatch;
|
||||||
|
|
||||||
|
if (Array.isArray(options.uid)) {
|
||||||
|
// batch list
|
||||||
|
query = options.uid.map(function(uid) {
|
||||||
|
return 'email_' + options.folder.path + (uid ? '_' + uid : '');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// single list
|
||||||
|
query = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._devicestorage.listItems(query, needsExactMatch);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1584,6 +1543,84 @@ Email.prototype._localDeleteMessage = function(options) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method that extracts a message body from the body parts
|
||||||
|
*
|
||||||
|
* @param {Object} message DTO
|
||||||
|
*/
|
||||||
|
Email.prototype._extractBody = function(message) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
}).then(function() {
|
||||||
|
// extract the content
|
||||||
|
if (message.encrypted) {
|
||||||
|
// show the encrypted message
|
||||||
|
message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = message.bodyParts;
|
||||||
|
|
||||||
|
if (message.signed) {
|
||||||
|
// PGP/MIME signed
|
||||||
|
var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest
|
||||||
|
message.signedMessage = signedRoot.signedMessage;
|
||||||
|
message.signature = signedRoot.signature;
|
||||||
|
root = signedRoot.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n');
|
||||||
|
|
||||||
|
// if the message is plain text and contains pgp/inline, we are only interested in the encrypted content, the rest (corporate mail footer, attachments, etc.) is discarded.
|
||||||
|
var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im.exec(body);
|
||||||
|
if (pgpInlineMatch) {
|
||||||
|
message.body = pgpInlineMatch[0]; // show the plain text content
|
||||||
|
message.encrypted = true; // signal the ui that we're handling encrypted content
|
||||||
|
|
||||||
|
// replace the bodyParts info with an artificial bodyPart of type "encrypted"
|
||||||
|
message.bodyParts = [{
|
||||||
|
type: MSG_PART_TYPE_ENCRYPTED,
|
||||||
|
content: pgpInlineMatch[0],
|
||||||
|
_isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// any content before/after the PGP block will be discarded, untrusted attachments and html is ignored
|
||||||
|
var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body);
|
||||||
|
if (clearSignedMatch) {
|
||||||
|
// PGP/INLINE signed
|
||||||
|
message.signed = true;
|
||||||
|
message.clearSignedMessage = clearSignedMatch[0];
|
||||||
|
body = clearSignedMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.signed) {
|
||||||
|
// message is not signed, so we're done here
|
||||||
|
return setBody(body, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the signatures for signed messages
|
||||||
|
return self._checkSignatures(message).then(function(signaturesValid) {
|
||||||
|
message.signed = typeof signaturesValid !== 'undefined';
|
||||||
|
message.signaturesValid = signaturesValid;
|
||||||
|
setBody(body, root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setBody(body, root) {
|
||||||
|
message.body = body;
|
||||||
|
if (!message.clearSignedMessage) {
|
||||||
|
message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT);
|
||||||
|
message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n');
|
||||||
|
inlineExternalImages(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an email using the mail reader
|
* Parse an email using the mail reader
|
||||||
* @param {Object} options The option to be passed to the mailreader
|
* @param {Object} options The option to be passed to the mailreader
|
||||||
|
@ -135,7 +135,7 @@ Outbox.prototype._processOutbox = function(callback) {
|
|||||||
self._outboxBusy = true;
|
self._outboxBusy = true;
|
||||||
|
|
||||||
// get pending mails from the outbox
|
// get pending mails from the outbox
|
||||||
self._devicestorage.listItems(outboxDb, 0, null).then(function(pendingMails) {
|
self._devicestorage.listItems(outboxDb).then(function(pendingMails) {
|
||||||
// if we're not online, don't even bother sending mails.
|
// if we're not online, don't even bother sending mails.
|
||||||
if (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
|
if (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
|
||||||
unsentMails = pendingMails.length;
|
unsentMails = pendingMails.length;
|
||||||
|
@ -297,7 +297,7 @@ Auth.prototype._loadCredentials = function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadFromDB(key) {
|
function loadFromDB(key) {
|
||||||
return self._appConfigStore.listItems(key, 0, null).then(function(cachedItems) {
|
return self._appConfigStore.listItems(key).then(function(cachedItems) {
|
||||||
return cachedItems && cachedItems[0];
|
return cachedItems && cachedItems[0];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -83,14 +83,13 @@ DeviceStorage.prototype.removeList = function(type) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List stored items of a given type
|
* List stored items of a given type
|
||||||
* @param type [String] The type of item e.g. 'email'
|
* @param {String/Array} query The type of item e.g. 'email'
|
||||||
* @param offset [Number] The offset of items to fetch (0 is the last stored item)
|
* @param {Boolean} exactMatchOnly Specifies if only exact matches are extracted from the DB as opposed to keys that start with the query
|
||||||
* @param num [Number] The number of items to fetch (null means fetch all)
|
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
DeviceStorage.prototype.listItems = function(type, offset, num) {
|
DeviceStorage.prototype.listItems = function(query, exactMatchOnly) {
|
||||||
// fetch all items of a certain type from the data-store
|
// fetch all items of a certain query from the data-store
|
||||||
return this._lawnchairDAO.list(type, offset, num);
|
return this._lawnchairDAO.list(query, exactMatchOnly);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,7 +147,7 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// search local keyring for public key
|
// search local keyring for public key
|
||||||
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
|
return self._lawnchairDAO.list(DB_PUBLICKEY).then(function(allPubkeys) {
|
||||||
var userIds;
|
var userIds;
|
||||||
// query primary email address
|
// query primary email address
|
||||||
var pubkey = _.findWhere(allPubkeys, {
|
var pubkey = _.findWhere(allPubkeys, {
|
||||||
@ -209,7 +209,7 @@ Keychain.prototype.getUserKeyPair = function(userId) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// search for user's public key locally
|
// search for user's public key locally
|
||||||
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
|
return self._lawnchairDAO.list(DB_PUBLICKEY).then(function(allPubkeys) {
|
||||||
var pubkey = _.findWhere(allPubkeys, {
|
var pubkey = _.findWhere(allPubkeys, {
|
||||||
userId: userId
|
userId: userId
|
||||||
});
|
});
|
||||||
@ -342,7 +342,7 @@ Keychain.prototype.lookupPublicKey = function(id) {
|
|||||||
*/
|
*/
|
||||||
Keychain.prototype.listLocalPublicKeys = function() {
|
Keychain.prototype.listLocalPublicKeys = function() {
|
||||||
// search local keyring for public key
|
// search local keyring for public key
|
||||||
return this._lawnchairDAO.list(DB_PUBLICKEY, 0, null);
|
return this._lawnchairDAO.list(DB_PUBLICKEY);
|
||||||
};
|
};
|
||||||
|
|
||||||
Keychain.prototype.removeLocalPublicKey = function(id) {
|
Keychain.prototype.removeLocalPublicKey = function(id) {
|
||||||
|
@ -104,60 +104,49 @@ LawnchairDAO.prototype.read = function(key) {
|
|||||||
/**
|
/**
|
||||||
* List all the items of a certain type
|
* List all the items of a certain type
|
||||||
* @param type [String] The type of item e.g. 'email'
|
* @param type [String] The type of item e.g. 'email'
|
||||||
* @param offset [Number] The offset of items to fetch (0 is the last stored item)
|
|
||||||
* @param num [Number] The number of items to fetch (null means fetch all)
|
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
LawnchairDAO.prototype.list = function(type, offset, num) {
|
LawnchairDAO.prototype.list = function(query, exactMatchOnly) {
|
||||||
var self = this;
|
var self = this;
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
var i, from, to,
|
var matchingKeys = [];
|
||||||
matchingKeys = [],
|
|
||||||
intervalKeys = [],
|
|
||||||
list = [];
|
|
||||||
|
|
||||||
// validate input
|
// validate input
|
||||||
if (!type || typeof offset === 'undefined' || typeof num === 'undefined') {
|
if ((Array.isArray(query) && query.length === 0) || (!Array.isArray(query) && !query)) {
|
||||||
throw new Error('Args not is not set!');
|
throw new Error('Args not is not set!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this method operates on arrays of keys, so normalize input 'key' -> ['key']
|
||||||
|
if (!Array.isArray(query)) {
|
||||||
|
query = [query];
|
||||||
|
}
|
||||||
|
|
||||||
// get all keys
|
// get all keys
|
||||||
self._db.keys(function(keys) {
|
self._db.keys(function(keys) {
|
||||||
// check if key begins with type
|
// check if there are keys in the db that start with the respective query
|
||||||
keys.forEach(function(key) {
|
matchingKeys = keys.filter(function(key) {
|
||||||
if (key.indexOf(type) === 0) {
|
return query.filter(function(type) {
|
||||||
matchingKeys.push(key);
|
if (exactMatchOnly) {
|
||||||
|
return key === type;
|
||||||
|
} else {
|
||||||
|
return key.indexOf(type) === 0;
|
||||||
}
|
}
|
||||||
|
}).length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort keys
|
if (matchingKeys.length === 0) {
|
||||||
matchingKeys.sort();
|
// no matching keys, resolve
|
||||||
|
resolve([]);
|
||||||
// set window of items to fetch
|
|
||||||
// if num is null, list all items
|
|
||||||
from = (num) ? matchingKeys.length - offset - num : 0;
|
|
||||||
to = matchingKeys.length - 1 - offset;
|
|
||||||
// filter items within requested interval
|
|
||||||
for (i = 0; i < matchingKeys.length; i++) {
|
|
||||||
if (i >= from && i <= to) {
|
|
||||||
intervalKeys.push(matchingKeys[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return if there are no matching keys
|
|
||||||
if (intervalKeys.length === 0) {
|
|
||||||
resolve(list);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch all items from data-store with matching key
|
// fetch all items from data-store with matching keys
|
||||||
self._db.get(intervalKeys, function(intervalList) {
|
self._db.get(matchingKeys, function(intervalList) {
|
||||||
intervalList.forEach(function(item) {
|
var result = intervalList.map(function(item) {
|
||||||
list.push(item.object);
|
return item.object;
|
||||||
});
|
});
|
||||||
|
|
||||||
// return only the interval between offset and num
|
resolve(result);
|
||||||
resolve(list);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -32,7 +32,7 @@ UpdateHandler.prototype.update = function() {
|
|||||||
targetVersion = cfg.dbVersion,
|
targetVersion = cfg.dbVersion,
|
||||||
versionDbType = 'dbVersion';
|
versionDbType = 'dbVersion';
|
||||||
|
|
||||||
return self._appConfigStorage.listItems(versionDbType, 0, null).then(function(items) {
|
return self._appConfigStorage.listItems(versionDbType).then(function(items) {
|
||||||
// parse the database version number
|
// parse the database version number
|
||||||
if (items && items.length > 0) {
|
if (items && items.length > 0) {
|
||||||
currentVersion = parseInt(items[0], 10);
|
currentVersion = parseInt(items[0], 10);
|
||||||
|
@ -66,7 +66,7 @@ function update(options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadFromDB(key) {
|
function loadFromDB(key) {
|
||||||
return options.appConfigStorage.listItems(key, 0, null).then(function(cachedItems) {
|
return options.appConfigStorage.listItems(key).then(function(cachedItems) {
|
||||||
return cachedItems && cachedItems[0];
|
return cachedItems && cachedItems[0];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ var POST_UPDATE_DB_VERSION = 5;
|
|||||||
*/
|
*/
|
||||||
function update(options) {
|
function update(options) {
|
||||||
// remove the emails
|
// remove the emails
|
||||||
return options.userStorage.listItems(FOLDER_DB_TYPE, 0, null).then(function(stored) {
|
return options.userStorage.listItems(FOLDER_DB_TYPE).then(function(stored) {
|
||||||
var folders = stored[0] || [];
|
var folders = stored[0] || [];
|
||||||
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
|
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
|
||||||
var foldersForType = folders.filter(function(mbx) {
|
var foldersForType = folders.filter(function(mbx) {
|
||||||
|
File diff suppressed because one or more lines are too long
@ -63,6 +63,21 @@ describe('Read Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('decrypt', function() {
|
||||||
|
it('should decrypt a message', function(done) {
|
||||||
|
var msg = {};
|
||||||
|
|
||||||
|
emailMock.decryptBody.withArgs({
|
||||||
|
message: msg
|
||||||
|
}).returns(resolves());
|
||||||
|
|
||||||
|
scope.decrypt(msg).then(function() {
|
||||||
|
expect(emailMock.decryptBody.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getKeyId', function() {
|
describe('getKeyId', function() {
|
||||||
var address = 'asfd@asdf.com';
|
var address = 'asfd@asdf.com';
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -68,12 +68,12 @@ describe('Auth unit tests', function() {
|
|||||||
|
|
||||||
describe('#getCredentials', function() {
|
describe('#getCredentials', function() {
|
||||||
it('should load credentials and retrieve credentials from cfg', function(done) {
|
it('should load credentials and retrieve credentials from cfg', function(done) {
|
||||||
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).returns(resolves([emailAddress]));
|
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).returns(resolves([emailAddress]));
|
||||||
storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).returns(resolves([encryptedPassword]));
|
storageStub.listItems.withArgs(PASSWD_DB_KEY).returns(resolves([encryptedPassword]));
|
||||||
storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).returns(resolves([username]));
|
storageStub.listItems.withArgs(USERNAME_DB_KEY).returns(resolves([username]));
|
||||||
storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).returns(resolves([realname]));
|
storageStub.listItems.withArgs(REALNAME_DB_KEY).returns(resolves([realname]));
|
||||||
storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).returns(resolves([imap]));
|
storageStub.listItems.withArgs(IMAP_DB_KEY).returns(resolves([imap]));
|
||||||
storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).returns(resolves([smtp]));
|
storageStub.listItems.withArgs(SMTP_DB_KEY).returns(resolves([smtp]));
|
||||||
pgpStub.decrypt.withArgs(encryptedPassword, undefined).returns(resolves({
|
pgpStub.decrypt.withArgs(encryptedPassword, undefined).returns(resolves({
|
||||||
decrypted: password,
|
decrypted: password,
|
||||||
signaturesValid: true
|
signaturesValid: true
|
||||||
@ -236,12 +236,12 @@ describe('Auth unit tests', function() {
|
|||||||
|
|
||||||
describe('#_loadCredentials', function() {
|
describe('#_loadCredentials', function() {
|
||||||
it('should work', function(done) {
|
it('should work', function(done) {
|
||||||
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).returns(resolves([emailAddress]));
|
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).returns(resolves([emailAddress]));
|
||||||
storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).returns(resolves([encryptedPassword]));
|
storageStub.listItems.withArgs(PASSWD_DB_KEY).returns(resolves([encryptedPassword]));
|
||||||
storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).returns(resolves([username]));
|
storageStub.listItems.withArgs(USERNAME_DB_KEY).returns(resolves([username]));
|
||||||
storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).returns(resolves([realname]));
|
storageStub.listItems.withArgs(REALNAME_DB_KEY).returns(resolves([realname]));
|
||||||
storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).returns(resolves([imap]));
|
storageStub.listItems.withArgs(IMAP_DB_KEY).returns(resolves([imap]));
|
||||||
storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).returns(resolves([smtp]));
|
storageStub.listItems.withArgs(SMTP_DB_KEY).returns(resolves([smtp]));
|
||||||
|
|
||||||
auth._loadCredentials().then(function() {
|
auth._loadCredentials().then(function() {
|
||||||
expect(auth.emailAddress).to.equal(emailAddress);
|
expect(auth.emailAddress).to.equal(emailAddress);
|
||||||
|
@ -82,7 +82,7 @@ describe('Device Storage DAO unit tests', function() {
|
|||||||
it('should work', function(done) {
|
it('should work', function(done) {
|
||||||
lawnchairDaoStub.list.returns(resolves());
|
lawnchairDaoStub.list.returns(resolves());
|
||||||
|
|
||||||
storageDao.listItems('email', 0, null).then(function() {
|
storageDao.listItems('email').then(function() {
|
||||||
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
|
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -54,7 +54,7 @@ describe('Keychain DAO unit tests', function() {
|
|||||||
|
|
||||||
describe('listLocalPublicKeys', function() {
|
describe('listLocalPublicKeys', function() {
|
||||||
it('should work', function(done) {
|
it('should work', function(done) {
|
||||||
lawnchairDaoStub.list.withArgs('publickey', 0, null).returns(resolves());
|
lawnchairDaoStub.list.withArgs('publickey').returns(resolves());
|
||||||
|
|
||||||
keychainDao.listLocalPublicKeys().then(function() {
|
keychainDao.listLocalPublicKeys().then(function() {
|
||||||
expect(lawnchairDaoStub.list.callCount).to.equal(1);
|
expect(lawnchairDaoStub.list.callCount).to.equal(1);
|
||||||
|
@ -43,7 +43,7 @@ describe('Lawnchair DAO unit tests', function() {
|
|||||||
|
|
||||||
describe('list', function() {
|
describe('list', function() {
|
||||||
it('should fail', function(done) {
|
it('should fail', function(done) {
|
||||||
lawnchairDao.list(undefined, 0, null).catch(function(err) {
|
lawnchairDao.list(undefined).catch(function(err) {
|
||||||
expect(err).to.exist;
|
expect(err).to.exist;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -106,13 +106,13 @@ describe('Lawnchair DAO unit tests', function() {
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
lawnchairDao.batch(list).then(function() {
|
lawnchairDao.batch(list).then(function() {
|
||||||
return lawnchairDao.list('type', 0, null);
|
return lawnchairDao.list('type');
|
||||||
}).then(function(fetched) {
|
}).then(function(fetched) {
|
||||||
expect(fetched.length).to.equal(2);
|
expect(fetched.length).to.equal(2);
|
||||||
expect(fetched[0]).to.deep.equal(list[0].object);
|
expect(fetched[0]).to.deep.equal(list[0].object);
|
||||||
return lawnchairDao.removeList('type');
|
return lawnchairDao.removeList('type');
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return lawnchairDao.list('type', 0, null);
|
return lawnchairDao.list('type');
|
||||||
}).then(function(fetched) {
|
}).then(function(fetched) {
|
||||||
expect(fetched).to.exist;
|
expect(fetched).to.exist;
|
||||||
expect(fetched.length).to.equal(0);
|
expect(fetched.length).to.equal(0);
|
||||||
|
Loading…
Reference in New Issue
Block a user