mirror of
https://github.com/moparisthebest/mail
synced 2024-11-22 00:42:20 -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-svgstore": "~0.3.4",
|
||||
"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",
|
||||
"mailbuild": "^0.3.7",
|
||||
"mailreader": "~0.4.0",
|
||||
|
@ -54,24 +54,16 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.getBody = function(message) {
|
||||
$scope.getBody = function(messages) {
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return email.getBody({
|
||||
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) {
|
||||
if (err.code !== 42) {
|
||||
dialog.error(err);
|
||||
@ -136,6 +128,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
||||
* Date formatting
|
||||
*/
|
||||
$scope.formatDate = function(date) {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof date === 'string') {
|
||||
date = new Date(date);
|
||||
}
|
||||
|
@ -112,9 +112,7 @@ var NavigationCtrl = function($scope, $location, $q, $timeout, account, email, o
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return email.refreshFolder({
|
||||
folder: ob
|
||||
});
|
||||
return email.refreshOutbox();
|
||||
|
||||
}).catch(dialog.error);
|
||||
};
|
||||
|
@ -52,6 +52,12 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
|
||||
status.setReading(false);
|
||||
};
|
||||
|
||||
$scope.decrypt = function(message) {
|
||||
return email.decryptBody({
|
||||
message: message
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getKeyId = function(address) {
|
||||
if ($location.search().dev || !address) {
|
||||
return;
|
||||
|
@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var PREFETCH_ITEMS = 10;
|
||||
|
||||
var ngModule = angular.module('woDirectives');
|
||||
|
||||
ngModule.directive('listScroll', function($timeout) {
|
||||
@ -20,7 +22,10 @@ ngModule.directive('listScroll', function($timeout) {
|
||||
inViewport = false,
|
||||
listItem, message,
|
||||
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) {
|
||||
// list not visible
|
||||
@ -38,7 +43,6 @@ ngModule.directive('listScroll', function($timeout) {
|
||||
}
|
||||
message = displayMessages[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 || 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
|
||||
inViewport = true;
|
||||
// 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) {
|
||||
// we are leaving the viewport, so stop iterating over the items
|
||||
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() {
|
||||
|
@ -92,7 +92,14 @@ ngModule.directive('frameLoad', function($window) {
|
||||
|
||||
function displayText(body) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ var FOLDER_TYPE_TRASH = 'Trash';
|
||||
var FOLDER_TYPE_FLAGGED = 'Flagged';
|
||||
|
||||
var MSG_ATTR_UID = 'uid';
|
||||
var MSG_ATTR_MODSEQ = 'modseq';
|
||||
var MSG_PART_ATTR_CONTENT = 'content';
|
||||
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
|
||||
var MSG_PART_TYPE_ENCRYPTED = 'encrypted';
|
||||
@ -83,13 +84,18 @@ function Email(keychain, pgp, accountStore, pgpbuilder, mailreader, dialog, appC
|
||||
* @resolve {Object} keypair
|
||||
*/
|
||||
Email.prototype.init = function(options) {
|
||||
this._account = options.account;
|
||||
this._account.busy = 0; // > 0 triggers the spinner
|
||||
this._account.online = false;
|
||||
this._account.loggingIn = false;
|
||||
var self = this;
|
||||
|
||||
// init folders from memory
|
||||
return this._initFoldersFromDisk();
|
||||
self._account = options.account;
|
||||
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.
|
||||
@ -520,173 +415,112 @@ Email.prototype.moveMessage = function(options) {
|
||||
*/
|
||||
Email.prototype.getBody = function(options) {
|
||||
var self = this,
|
||||
message = options.message,
|
||||
folder = options.folder,
|
||||
localMessage, attachmentParts;
|
||||
messages = options.messages,
|
||||
folder = options.folder;
|
||||
|
||||
// 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') {
|
||||
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
|
||||
return !(message.loadingBody || typeof message.body !== 'undefined');
|
||||
});
|
||||
|
||||
if (!messages.length) {
|
||||
return new Promise(function(resolve) {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
message.loadingBody = true;
|
||||
messages.forEach(function(message) {
|
||||
message.loadingBody = true;
|
||||
});
|
||||
|
||||
self.busy();
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
var loadedMessages;
|
||||
|
||||
// load the local message from memory
|
||||
// load the message from disk
|
||||
return self._localListMessages({
|
||||
folder: folder,
|
||||
uid: message.uid
|
||||
uid: _.pluck(messages, MSG_ATTR_UID)
|
||||
}).then(function(localMessages) {
|
||||
localMessage = localMessages[0];
|
||||
loadedMessages = localMessages;
|
||||
|
||||
if (!localMessage) {
|
||||
// the message has been deleted in the meantime
|
||||
var error = new Error('Can not get the contents of this message. It has already been deleted!');
|
||||
error.hide = true;
|
||||
throw error;
|
||||
// find out which messages are not available on disk (uids not included in disk roundtrip)
|
||||
var localUids = _.pluck(localMessages, MSG_ATTR_UID);
|
||||
var needsImapFetch = messages.filter(function(msg) {
|
||||
return !_.contains(localUids, msg.uid);
|
||||
});
|
||||
return needsImapFetch;
|
||||
|
||||
}).then(function(needsImapFetch) {
|
||||
// get the missing messages from imap
|
||||
|
||||
if (!needsImapFetch.length) {
|
||||
// no imap roundtrip needed, we're done
|
||||
return loadedMessages;
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
// do the imap roundtrip
|
||||
return self._fetchMessages({
|
||||
messages: needsImapFetch,
|
||||
folder: folder
|
||||
}).then(function(imapMessages) {
|
||||
// add the messages from imap to the loaded messages
|
||||
loadedMessages = loadedMessages.concat(imapMessages);
|
||||
|
||||
// do we need to fetch content from the imap server?
|
||||
var needsFetch = false;
|
||||
contentParts.forEach(function(part) {
|
||||
needsFetch = (typeof part.content === 'undefined');
|
||||
});
|
||||
}).catch(function(err) {
|
||||
axe.error('Can not fetch messages from IMAP. Reason: ' + err.message + (err.stack ? ('\n' + err.stack) : ''));
|
||||
|
||||
if (!needsFetch) {
|
||||
// if we have all the content we need,
|
||||
// we can extract the content
|
||||
message.bodyParts = localMessage.bodyParts;
|
||||
return;
|
||||
}
|
||||
|
||||
// get the raw content from the imap server
|
||||
return self._getBodyParts({
|
||||
folder: folder,
|
||||
uid: localMessage.uid,
|
||||
bodyParts: contentParts
|
||||
}).then(function(parsedBodyParts) {
|
||||
// piece together the parsed bodyparts and the empty attachments which have not been parsed
|
||||
message.bodyParts = parsedBodyParts.concat(attachmentParts);
|
||||
localMessage.bodyParts = parsedBodyParts.concat(attachmentParts);
|
||||
|
||||
// persist it to disk
|
||||
return self._localStoreMessages({
|
||||
folder: folder,
|
||||
emails: [localMessage]
|
||||
// stop the loading spinner for those messages we can't fetch
|
||||
needsImapFetch.forEach(function(message) {
|
||||
message.loadingBody = false;
|
||||
});
|
||||
|
||||
// we can't fetch from imap, just continue with what we have
|
||||
messages = _.difference(messages, needsImapFetch);
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// extract the content
|
||||
if (message.encrypted) {
|
||||
// show the encrypted message
|
||||
message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content;
|
||||
return;
|
||||
}
|
||||
// enhance dummy messages with content
|
||||
messages.forEach(function(message) {
|
||||
var loadedMessage = _.findWhere(loadedMessages, {
|
||||
uid: message.uid
|
||||
});
|
||||
|
||||
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);
|
||||
// enhance the dummy message with the loaded content
|
||||
_.extend(message, loadedMessage);
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
self.done();
|
||||
message.loadingBody = false;
|
||||
return message;
|
||||
// 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) {
|
||||
self.done();
|
||||
message.loadingBody = false;
|
||||
if (err.hide) {
|
||||
// ignore errors with err.hide
|
||||
return message;
|
||||
}
|
||||
done();
|
||||
throw err;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
function done() {
|
||||
messages.forEach(function(message) {
|
||||
message.loadingBody = false;
|
||||
});
|
||||
self.done();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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() {
|
||||
self._account.loggingIn = false;
|
||||
// init folders
|
||||
return self._initFoldersFromImap();
|
||||
return self._updateFolders();
|
||||
|
||||
}).then(function() {
|
||||
// fill the imap mailboxCache with information we have locally available:
|
||||
@ -997,31 +872,17 @@ Email.prototype.onConnect = function(imap) {
|
||||
// - next expected uid
|
||||
var mailboxCache = {};
|
||||
self._account.folders.forEach(function(folder) {
|
||||
if (folder.messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var uids, highestModseq, lastUid;
|
||||
|
||||
uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) {
|
||||
var uids = folder.uids.sort(function(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] = {
|
||||
exists: lastUid,
|
||||
uidNext: lastUid + 1,
|
||||
uidlist: uids,
|
||||
highestModseq: highestModseq
|
||||
highestModseq: '' + folder.modseq
|
||||
};
|
||||
});
|
||||
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
|
||||
*/
|
||||
Email.prototype._onSyncUpdate = function(options) {
|
||||
var self = this;
|
||||
var self = this,
|
||||
uids = options.list;
|
||||
|
||||
var folder = _.findWhere(self._account.folders, {
|
||||
path: options.path
|
||||
});
|
||||
|
||||
if (!folder) {
|
||||
// ignore updates for an unknown folder
|
||||
return;
|
||||
return; // ignore updates for an unknown folder
|
||||
}
|
||||
|
||||
if (options.type === SYNC_TYPE_NEW) {
|
||||
// new messages available on imap, fetch from imap and store to disk and memory
|
||||
self.fetchMessages({
|
||||
folder: folder,
|
||||
firstUid: Math.min.apply(null, options.list),
|
||||
lastUid: Math.max.apply(null, options.list)
|
||||
}).then(self._dialog.error).catch(self._dialog.error);
|
||||
// new messages available on imap, add the new uids to the folder
|
||||
|
||||
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,
|
||||
messages: fetch,
|
||||
notifyNew: folder.type === FOLDER_TYPE_INBOX
|
||||
}).catch(self._dialog.error);
|
||||
}
|
||||
|
||||
} else if (options.type === SYNC_TYPE_DELETED) {
|
||||
// messages have been deleted, remove from local storage and memory
|
||||
options.list.forEach(function(uid) {
|
||||
// messages have been deleted
|
||||
|
||||
folder.uids = _.difference(folder.uids, uids); // remove the uids from the uid list
|
||||
uids.forEach(function(uid) {
|
||||
var message = _.findWhere(folder.messages, {
|
||||
uid: uid
|
||||
});
|
||||
@ -1132,12 +1020,12 @@ Email.prototype._onSyncUpdate = function(options) {
|
||||
folder: folder,
|
||||
message: message,
|
||||
localOnly: true
|
||||
}).then(self._dialog.error).catch(self._dialog.error);
|
||||
}).catch(self._dialog.error);
|
||||
});
|
||||
} else if (options.type === SYNC_TYPE_MSGS) {
|
||||
// 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
|
||||
options.list.forEach(function(changedMsg) {
|
||||
uids.forEach(function(changedMsg) {
|
||||
if (!changedMsg.uid || !changedMsg.flags) {
|
||||
return;
|
||||
}
|
||||
@ -1146,7 +1034,7 @@ Email.prototype._onSyncUpdate = function(options) {
|
||||
uid: changedMsg.uid
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
if (!message || !message.bodyParts) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1159,7 +1047,14 @@ Email.prototype._onSyncUpdate = function(options) {
|
||||
folder: folder,
|
||||
message: message,
|
||||
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,
|
||||
* 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.
|
||||
*/
|
||||
Email.prototype._initFoldersFromImap = function() {
|
||||
Email.prototype._updateFolders = function() {
|
||||
var self = this;
|
||||
|
||||
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 (!foldersChanged) {
|
||||
return;
|
||||
if (foldersChanged) {
|
||||
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() {
|
||||
return self._initMessagesFromDisk();
|
||||
return self._initFolders();
|
||||
|
||||
}).then(function() {
|
||||
self.done();
|
||||
@ -1342,28 +1200,32 @@ Email.prototype._initFoldersFromImap = function() {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fill uninitialized folders with the locally available messages.
|
||||
*/
|
||||
Email.prototype._initMessagesFromDisk = function() {
|
||||
Email.prototype._initFolders = function() {
|
||||
var self = this;
|
||||
|
||||
var jobs = [];
|
||||
self._account.folders.forEach(function(folder) {
|
||||
if (folder.messages) {
|
||||
// the folder is already initialized
|
||||
return;
|
||||
}
|
||||
|
||||
// sync messages from disk to the folder model
|
||||
jobs.push(self.refreshFolder({
|
||||
folder: folder
|
||||
}));
|
||||
folder.modseq = folder.modseq || 0;
|
||||
folder.uids = folder.uids || []; // attach an empty uids array to the folder
|
||||
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
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(jobs).then(function() {
|
||||
return; // don't return promise array
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
*
|
||||
@ -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
|
||||
* @param {String} options.folder The folder
|
||||
* @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) {
|
||||
var self = this;
|
||||
@ -1513,8 +1441,8 @@ Email.prototype._getBodyParts = function(options) {
|
||||
return self._imapClient.getBodyParts(options);
|
||||
}).then(function() {
|
||||
if (options.bodyParts.filter(function(bodyPart) {
|
||||
return !(bodyPart.raw || bodyPart.content);
|
||||
}).length) {
|
||||
return !(bodyPart.raw || bodyPart.content);
|
||||
}).length) {
|
||||
var error = new Error('Can not get the contents of this message. It has already been deleted!');
|
||||
error.hide = true;
|
||||
throw error;
|
||||
@ -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),
|
||||
@ -1540,8 +1486,21 @@ Email.prototype._getBodyParts = function(options) {
|
||||
* @param {Object} options.uid A specific uid to look up locally in the folder
|
||||
*/
|
||||
Email.prototype._localListMessages = function(options) {
|
||||
var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : '');
|
||||
return this._devicestorage.listItems(dbType, 0, null);
|
||||
var query;
|
||||
|
||||
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
|
||||
* @param {Object} options The option to be passed to the mailreader
|
||||
|
@ -135,7 +135,7 @@ Outbox.prototype._processOutbox = function(callback) {
|
||||
self._outboxBusy = true;
|
||||
|
||||
// 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 (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
|
||||
unsentMails = pendingMails.length;
|
||||
|
@ -297,7 +297,7 @@ Auth.prototype._loadCredentials = function() {
|
||||
});
|
||||
|
||||
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];
|
||||
});
|
||||
}
|
||||
|
@ -83,14 +83,13 @@ DeviceStorage.prototype.removeList = function(type) {
|
||||
|
||||
/**
|
||||
* List stored items of a given type
|
||||
* @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)
|
||||
* @param {String/Array} query The type of item e.g. 'email'
|
||||
* @param {Boolean} exactMatchOnly Specifies if only exact matches are extracted from the DB as opposed to keys that start with the query
|
||||
* @return {Promise}
|
||||
*/
|
||||
DeviceStorage.prototype.listItems = function(type, offset, num) {
|
||||
// fetch all items of a certain type from the data-store
|
||||
return this._lawnchairDAO.list(type, offset, num);
|
||||
DeviceStorage.prototype.listItems = function(query, exactMatchOnly) {
|
||||
// fetch all items of a certain query from the data-store
|
||||
return this._lawnchairDAO.list(query, exactMatchOnly);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -147,7 +147,7 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
|
||||
var self = this;
|
||||
|
||||
// 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;
|
||||
// query primary email address
|
||||
var pubkey = _.findWhere(allPubkeys, {
|
||||
@ -209,7 +209,7 @@ Keychain.prototype.getUserKeyPair = function(userId) {
|
||||
var self = this;
|
||||
|
||||
// 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, {
|
||||
userId: userId
|
||||
});
|
||||
@ -342,7 +342,7 @@ Keychain.prototype.lookupPublicKey = function(id) {
|
||||
*/
|
||||
Keychain.prototype.listLocalPublicKeys = function() {
|
||||
// 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) {
|
||||
|
@ -104,60 +104,49 @@ LawnchairDAO.prototype.read = function(key) {
|
||||
/**
|
||||
* List all the items of a certain type
|
||||
* @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}
|
||||
*/
|
||||
LawnchairDAO.prototype.list = function(type, offset, num) {
|
||||
LawnchairDAO.prototype.list = function(query, exactMatchOnly) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
var i, from, to,
|
||||
matchingKeys = [],
|
||||
intervalKeys = [],
|
||||
list = [];
|
||||
var matchingKeys = [];
|
||||
|
||||
// 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!');
|
||||
}
|
||||
|
||||
// this method operates on arrays of keys, so normalize input 'key' -> ['key']
|
||||
if (!Array.isArray(query)) {
|
||||
query = [query];
|
||||
}
|
||||
|
||||
// get all keys
|
||||
self._db.keys(function(keys) {
|
||||
// check if key begins with type
|
||||
keys.forEach(function(key) {
|
||||
if (key.indexOf(type) === 0) {
|
||||
matchingKeys.push(key);
|
||||
}
|
||||
// check if there are keys in the db that start with the respective query
|
||||
matchingKeys = keys.filter(function(key) {
|
||||
return query.filter(function(type) {
|
||||
if (exactMatchOnly) {
|
||||
return key === type;
|
||||
} else {
|
||||
return key.indexOf(type) === 0;
|
||||
}
|
||||
}).length > 0;
|
||||
});
|
||||
|
||||
// sort keys
|
||||
matchingKeys.sort();
|
||||
|
||||
// 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);
|
||||
if (matchingKeys.length === 0) {
|
||||
// no matching keys, resolve
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch all items from data-store with matching key
|
||||
self._db.get(intervalKeys, function(intervalList) {
|
||||
intervalList.forEach(function(item) {
|
||||
list.push(item.object);
|
||||
// fetch all items from data-store with matching keys
|
||||
self._db.get(matchingKeys, function(intervalList) {
|
||||
var result = intervalList.map(function(item) {
|
||||
return item.object;
|
||||
});
|
||||
|
||||
// return only the interval between offset and num
|
||||
resolve(list);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -32,7 +32,7 @@ UpdateHandler.prototype.update = function() {
|
||||
targetVersion = cfg.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
|
||||
if (items && items.length > 0) {
|
||||
currentVersion = parseInt(items[0], 10);
|
||||
|
@ -66,7 +66,7 @@ function update(options) {
|
||||
});
|
||||
|
||||
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];
|
||||
});
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ var POST_UPDATE_DB_VERSION = 5;
|
||||
*/
|
||||
function update(options) {
|
||||
// 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] || [];
|
||||
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
|
||||
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() {
|
||||
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() {
|
||||
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(PASSWD_DB_KEY, 0, null).returns(resolves([encryptedPassword]));
|
||||
storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).returns(resolves([username]));
|
||||
storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).returns(resolves([realname]));
|
||||
storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).returns(resolves([imap]));
|
||||
storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).returns(resolves([smtp]));
|
||||
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).returns(resolves([emailAddress]));
|
||||
storageStub.listItems.withArgs(PASSWD_DB_KEY).returns(resolves([encryptedPassword]));
|
||||
storageStub.listItems.withArgs(USERNAME_DB_KEY).returns(resolves([username]));
|
||||
storageStub.listItems.withArgs(REALNAME_DB_KEY).returns(resolves([realname]));
|
||||
storageStub.listItems.withArgs(IMAP_DB_KEY).returns(resolves([imap]));
|
||||
storageStub.listItems.withArgs(SMTP_DB_KEY).returns(resolves([smtp]));
|
||||
pgpStub.decrypt.withArgs(encryptedPassword, undefined).returns(resolves({
|
||||
decrypted: password,
|
||||
signaturesValid: true
|
||||
@ -236,12 +236,12 @@ describe('Auth unit tests', function() {
|
||||
|
||||
describe('#_loadCredentials', function() {
|
||||
it('should work', function(done) {
|
||||
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).returns(resolves([emailAddress]));
|
||||
storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).returns(resolves([encryptedPassword]));
|
||||
storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).returns(resolves([username]));
|
||||
storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).returns(resolves([realname]));
|
||||
storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).returns(resolves([imap]));
|
||||
storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).returns(resolves([smtp]));
|
||||
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).returns(resolves([emailAddress]));
|
||||
storageStub.listItems.withArgs(PASSWD_DB_KEY).returns(resolves([encryptedPassword]));
|
||||
storageStub.listItems.withArgs(USERNAME_DB_KEY).returns(resolves([username]));
|
||||
storageStub.listItems.withArgs(REALNAME_DB_KEY).returns(resolves([realname]));
|
||||
storageStub.listItems.withArgs(IMAP_DB_KEY).returns(resolves([imap]));
|
||||
storageStub.listItems.withArgs(SMTP_DB_KEY).returns(resolves([smtp]));
|
||||
|
||||
auth._loadCredentials().then(function() {
|
||||
expect(auth.emailAddress).to.equal(emailAddress);
|
||||
|
@ -82,7 +82,7 @@ describe('Device Storage DAO unit tests', function() {
|
||||
it('should work', function(done) {
|
||||
lawnchairDaoStub.list.returns(resolves());
|
||||
|
||||
storageDao.listItems('email', 0, null).then(function() {
|
||||
storageDao.listItems('email').then(function() {
|
||||
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
|
@ -54,7 +54,7 @@ describe('Keychain DAO unit tests', function() {
|
||||
|
||||
describe('listLocalPublicKeys', function() {
|
||||
it('should work', function(done) {
|
||||
lawnchairDaoStub.list.withArgs('publickey', 0, null).returns(resolves());
|
||||
lawnchairDaoStub.list.withArgs('publickey').returns(resolves());
|
||||
|
||||
keychainDao.listLocalPublicKeys().then(function() {
|
||||
expect(lawnchairDaoStub.list.callCount).to.equal(1);
|
||||
|
@ -43,7 +43,7 @@ describe('Lawnchair DAO unit tests', function() {
|
||||
|
||||
describe('list', function() {
|
||||
it('should fail', function(done) {
|
||||
lawnchairDao.list(undefined, 0, null).catch(function(err) {
|
||||
lawnchairDao.list(undefined).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
@ -106,13 +106,13 @@ describe('Lawnchair DAO unit tests', function() {
|
||||
}];
|
||||
|
||||
lawnchairDao.batch(list).then(function() {
|
||||
return lawnchairDao.list('type', 0, null);
|
||||
return lawnchairDao.list('type');
|
||||
}).then(function(fetched) {
|
||||
expect(fetched.length).to.equal(2);
|
||||
expect(fetched[0]).to.deep.equal(list[0].object);
|
||||
return lawnchairDao.removeList('type');
|
||||
}).then(function() {
|
||||
return lawnchairDao.list('type', 0, null);
|
||||
return lawnchairDao.list('type');
|
||||
}).then(function(fetched) {
|
||||
expect(fetched).to.exist;
|
||||
expect(fetched.length).to.equal(0);
|
||||
|
Loading…
Reference in New Issue
Block a user