1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-26 02:42:17 -05:00

add documentation to email dao

This commit is contained in:
Felix Hammerl 2014-06-03 12:15:41 +02:00
parent 088519f4f8
commit c71e1482c9
2 changed files with 221 additions and 25 deletions

View File

@ -6,6 +6,16 @@ define(function(require) {
config = require('js/app-config').config, config = require('js/app-config').config,
str = require('js/app-config').string; str = require('js/app-config').string;
/**
* High-level data access object that orchestrates everything around the handling of encrypted mails:
* PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence
*
* @param {Object} keychain The keychain DAO handles keys transparently
* @param {Object} crypto Orchestrates decryption
* @param {Object} devicestorage Handles persistence to the local indexed db
* @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages
* @param {Object} mailreader Parses MIME messages received from IMAP
*/
var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) { var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) {
this._keychain = keychain; this._keychain = keychain;
this._crypto = crypto; this._crypto = crypto;
@ -22,6 +32,16 @@ define(function(require) {
// //
/**
* Initializes the email dao:
* - validates the email address
* - retrieves the user's key pair (if available)
* - initializes _account.folders with the content from memory
*
* @param {Object} options.account The account
* @param {String} options.account.emailAddress The user's id
* @param {Function} callback(error, keypair) Invoked with the keypair or error information when the email dao is initialized
*/
EmailDAO.prototype.init = function(options, callback) { EmailDAO.prototype.init = function(options, callback) {
var self = this, var self = this,
keypair; keypair;
@ -70,6 +90,11 @@ define(function(require) {
} }
}; };
/**
* Unlocks the keychain by either decrypting an existing private key or generating a new keypair
* @param {String} options.passphrase The passphrase to decrypt the private key
* @param {Function} callback(error) Invoked when the the keychain is unlocked or when an error occurred buring unlocking
*/
EmailDAO.prototype.unlock = function(options, callback) { EmailDAO.prototype.unlock = function(options, callback) {
var self = this; var self = this;
@ -170,6 +195,14 @@ define(function(require) {
} }
}; };
/**
* Opens a folder in IMAP so that we can receive updates for it.
* Please note that this is a no-op if you try to open the outbox, since it is not an IMAP folder
* but a virtual folder that only exists on disk.
*
* @param {Object} options.folder The folder to be opened
* @param {Function} callback(error) Invoked when the folder has been opened
*/
EmailDAO.prototype.openFolder = function(options, callback) { EmailDAO.prototype.openFolder = function(options, callback) {
var self = this, var self = this,
err; err;
@ -190,6 +223,14 @@ define(function(require) {
}, callback); }, callback);
}; };
/**
* 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
* @param {Function} callback [description]
*/
EmailDAO.prototype.refreshFolder = function(options, callback) { EmailDAO.prototype.refreshFolder = function(options, callback) {
var self = this, var self = this,
folder = options.folder; folder = options.folder;
@ -205,8 +246,8 @@ define(function(require) {
var storedUids = _.pluck(storedMessages, 'uid'), var storedUids = _.pluck(storedMessages, 'uid'),
memoryUids = _.pluck(folder.messages, 'uid'), memoryUids = _.pluck(folder.messages, 'uid'),
newUids = _.difference(storedUids, memoryUids), newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory
removedUids = _.difference(memoryUids, storedUids); 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? // which messages are new on the disk that are not yet in memory?
_.filter(storedMessages, function(msg) { _.filter(storedMessages, function(msg) {
@ -240,6 +281,15 @@ define(function(require) {
} }
}; };
/**
* Fetches a message's headers from IMAP.
*
* NB! If we fetch a message whose subject line correspond's to that of a verification message,
* we try to verify that, and if that worked, we delete the verified message from IMAP.
*
* @param {Object} options.folder The folder for which to fetch the message
* @param {Function} callback(error) Invoked when the message is persisted and added to folder.messages
*/
EmailDAO.prototype.fetchMessages = function(options, callback) { EmailDAO.prototype.fetchMessages = function(options, callback) {
var self = this, var self = this,
folder = options.folder; folder = options.folder;
@ -328,6 +378,7 @@ define(function(require) {
callback(err); callback(err);
} }
// Handles verification of public keys, deletion of messages with verified keys
function handleVerification(message, localCallback) { function handleVerification(message, localCallback) {
self._getBodyParts({ self._getBodyParts({
folder: folder, folder: folder,
@ -375,6 +426,17 @@ define(function(require) {
} }
}; };
/**
* Delete a message from IMAP, disk and folder.messages.
*
* Please note that this deletes from disk only if you delete from the outbox,
* since it is not an IMAP folder but a virtual folder that only exists on disk.
*
* @param {Object} options.folder The folder from which to delete the messages
* @param {Object} options.message The message that should be deleted
* @param {Boolean} options.localOnly Indicated if the message should not be removed from IMAP
* @param {Function} callback(error) Invoked when the message was delete, or an error occurred
*/
EmailDAO.prototype.deleteMessage = function(options, callback) { EmailDAO.prototype.deleteMessage = function(options, callback) {
var self = this, var self = this,
folder = options.folder, folder = options.folder,
@ -384,6 +446,7 @@ define(function(require) {
folder.messages.splice(folder.messages.indexOf(message), 1); folder.messages.splice(folder.messages.indexOf(message), 1);
// delete only locally
if (options.localOnly || options.folder.path === config.outboxMailboxPath) { if (options.localOnly || options.folder.path === config.outboxMailboxPath) {
deleteLocal(); deleteLocal();
return; return;
@ -393,6 +456,7 @@ define(function(require) {
function deleteImap() { function deleteImap() {
if (!self._account.online) { if (!self._account.online) {
// no action if we're not online
done({ done({
errMsg: 'Client is currently offline!', errMsg: 'Client is currently offline!',
code: 42 code: 42
@ -400,6 +464,7 @@ define(function(require) {
return; return;
} }
// delete from IMAP
self._imapDeleteMessage({ self._imapDeleteMessage({
folder: folder, folder: folder,
uid: message.uid uid: message.uid
@ -414,6 +479,7 @@ define(function(require) {
} }
function deleteLocal() { function deleteLocal() {
// delete from indexed db
self._localDeleteMessage({ self._localDeleteMessage({
folder: folder, folder: folder,
uid: message.uid uid: message.uid
@ -430,24 +496,37 @@ define(function(require) {
} }
}; };
/**
* Updates a message's 'unread' and 'answered' flags
*
* Please note if you set flags on disk only if you delete from the outbox,
* since it is not an IMAP folder but a virtual folder that only exists on disk.
*
* @param {[type]} options [description]
* @param {Function} callback [description]
*/
EmailDAO.prototype.setFlags = function(options, callback) { EmailDAO.prototype.setFlags = function(options, callback) {
var self = this, var self = this,
folder = options.folder, folder = options.folder,
message = options.message; message = options.message;
self._account.busy = true; self._account.busy = true; // start the spinner
// no-op if the message if not present anymore (for whatever reason)
if (folder.messages.indexOf(message) < 0) { if (folder.messages.indexOf(message) < 0) {
self._account.busy = false; // stop the spinner self._account.busy = false; // stop the spinner
return; return;
} }
// don't do a roundtrip to IMAP,
// especially if you want to mark outbox messages
if (options.localOnly || options.folder.path === config.outboxMailboxPath) { if (options.localOnly || options.folder.path === config.outboxMailboxPath) {
markStorage(); markStorage();
return; return;
} }
if (!self._account.online) { if (!self._account.online) {
// no action if we're not online
done({ done({
errMsg: 'Client is currently offline!', errMsg: 'Client is currently offline!',
code: 42 code: 42
@ -458,6 +537,7 @@ define(function(require) {
markImap(); markImap();
function markImap() { function markImap() {
// mark a message unread/answered on IMAP
self._imapMark({ self._imapMark({
folder: folder, folder: folder,
uid: options.message.uid, uid: options.message.uid,
@ -474,6 +554,9 @@ define(function(require) {
} }
function markStorage() { function markStorage() {
// angular pollutes that data transfer objects with helper properties (e.g. $$hashKey),
// which we do not want to persist to disk. in order to avoid that, we load the pristine
// message from disk, change the flags and re-persist it to disk
self._localListMessages({ self._localListMessages({
folder: folder, folder: folder,
uid: options.message.uid, uid: options.message.uid,
@ -483,10 +566,13 @@ define(function(require) {
return; return;
} }
// set the flags
var storedMessage = storedMessages[0]; var storedMessage = storedMessages[0];
storedMessage.unread = options.message.unread; storedMessage.unread = options.message.unread;
storedMessage.answered = options.message.answered; storedMessage.answered = options.message.answered;
storedMessage.modseq = options.message.modseq || storedMessage.modseq;
// store
self._localStoreMessages({ self._localStoreMessages({
folder: folder, folder: folder,
emails: [storedMessage] emails: [storedMessage]
@ -663,6 +749,14 @@ define(function(require) {
} }
}; };
/**
* Retrieves an attachment matching a body part for a given uid and a folder
*
* @param {Object} options.folder The folder where to find the attachment
* @param {Number} options.uid The uid for the message the attachment body part belongs to
* @param {Object} options.attachment The attachment body part to fetch and parse from IMAP
* @param {Function} callback(error, attachment) Invoked when the attachment body part was retrieved and parsed, or an error occurred
*/
EmailDAO.prototype.getAttachment = function(options, callback) { EmailDAO.prototype.getAttachment = function(options, callback) {
this._getBodyParts({ this._getBodyParts({
folder: options.folder, folder: options.folder,
@ -674,11 +768,19 @@ define(function(require) {
return; return;
} }
// add the content to the original object
options.attachment.content = parsedBodyParts[0].content; options.attachment.content = parsedBodyParts[0].content;
callback(err, err ? undefined : options.attachment); callback(err, err ? undefined : options.attachment);
}); });
}; };
/**
* Decrypts a message and replaces sets the decrypted plaintext as the message's body, html, or attachment, respectively.
* The first encrypted body part's ciphertext (in the content property) will be decrypted.
*
* @param {Object} options.message The message
* @param {Function} callback(error, message)
*/
EmailDAO.prototype.decryptBody = function(options, callback) { EmailDAO.prototype.decryptBody = function(options, callback) {
var self = this, var self = this,
message = options.message; message = options.message;
@ -762,6 +864,12 @@ define(function(require) {
} }
}; };
/**
* Encrypted (if necessary) and sends a message with a predefined clear text greeting.
*
* @param {Object} options.email The message to be sent
* @param {Function} callback(error) Invoked when the message was sent, or an error occurred
*/
EmailDAO.prototype.sendEncrypted = function(options, callback) { EmailDAO.prototype.sendEncrypted = function(options, callback) {
var self = this; var self = this;
@ -782,6 +890,12 @@ define(function(require) {
}, callback); }, callback);
}; };
/**
* Sends a signed message in the plain
*
* @param {Object} options.email The message to be sent
* @param {Function} callback(error) Invoked when the message was sent, or an error occurred
*/
EmailDAO.prototype.sendPlaintext = function(options, callback) { EmailDAO.prototype.sendPlaintext = function(options, callback) {
if (!this._account.online) { if (!this._account.online) {
callback({ callback({
@ -797,6 +911,12 @@ define(function(require) {
}, callback); }, callback);
}; };
/**
* Signs and encrypts a message
*
* @param {Object} options.email The message to be encrypted
* @param {Function} callback(error, message) Invoked when the message was encrypted, or an error occurred
*/
EmailDAO.prototype.encrypt = function(options, callback) { EmailDAO.prototype.encrypt = function(options, callback) {
this._pgpbuilder.encrypt(options, callback); this._pgpbuilder.encrypt(options, callback);
}; };
@ -809,6 +929,15 @@ define(function(require) {
// //
/**
* This handler should be invoked when navigator.onLine === true. It will try to connect a
* given instance of the imap client. If the connection attempt was successful, it will
* update the locally available folders with the newly received IMAP folder listing.
*
* @param {Object} options.imapClient The IMAP client used to receive messages
* @param {Object} options.pgpMailer The SMTP client used to send messages
* @param {Function} callback [description]
*/
EmailDAO.prototype.onConnect = function(options, callback) { EmailDAO.prototype.onConnect = function(options, callback) {
var self = this; var self = this;
@ -838,7 +967,11 @@ define(function(require) {
// attach sync update handler // attach sync update handler
self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self); self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self);
// fill the imap mailboxCache // fill the imap mailboxCache with information we have locally available:
// - highest locally available moseq
// - list of locally available uids
// - highest locally available 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) { if (folder.messages.length === 0) {
@ -865,6 +998,7 @@ define(function(require) {
}); });
self._imapClient.mailboxCache = mailboxCache; self._imapClient.mailboxCache = mailboxCache;
// set up the imap client to listen for changes in the inbox
var inbox = _.findWhere(self._account.folders, { var inbox = _.findWhere(self._account.folders, {
type: 'Inbox' type: 'Inbox'
}); });
@ -881,12 +1015,26 @@ define(function(require) {
}); });
}; };
/**
* This handler should be invoked when navigator.onLine === false. It will discard
* the imap client and pgp mailer
*/
EmailDAO.prototype.onDisconnect = function() { EmailDAO.prototype.onDisconnect = function() {
this._account.online = false; this._account.online = false;
this._imapClient = undefined; this._imapClient = undefined;
this._pgpMailer = undefined; this._pgpMailer = undefined;
}; };
/**
* The are updates in the IMAP folder of the following type
* - 'new': a list of uids that are newly available
* - 'deleted': a list of uids that were deleted from IMAP available
* - 'messages': a list of messages (uid + flags) that where changes are available
*
* @param {String} options.type The type of the update
* @param {String} options.path The mailbox for which updates are available
* @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively
*/
EmailDAO.prototype._onSyncUpdate = function(options) { EmailDAO.prototype._onSyncUpdate = function(options) {
var self = this; var self = this;
@ -939,16 +1087,15 @@ define(function(require) {
return; return;
} }
// update unread, answered, modseq to the latest info
message.answered = changedMsg.flags.indexOf('\\Answered') > -1; message.answered = changedMsg.flags.indexOf('\\Answered') > -1;
message.unread = changedMsg.flags.indexOf('\\Seen') === -1; message.unread = changedMsg.flags.indexOf('\\Seen') === -1;
message.modseq = changedMsg.modseq;
if (!message) {
return;
}
self.setFlags({ self.setFlags({
folder: folder, folder: folder,
message: message message: message,
localOnly: true
}, self.onError.bind(self)); }, self.onError.bind(self));
}); });
} }
@ -963,14 +1110,18 @@ define(function(require) {
/** /**
* List the folders in the user's IMAP mailbox. * Updates the folder information from memory (if we're offline), or from imap (if we're online),
* and 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
*
* @param {Function} callback Invoked when the folders are up to date
*/ */
EmailDAO.prototype._initFolders = function(callback) { EmailDAO.prototype._initFolders = function(callback) {
var self = this, var self = this,
folderDbType = 'folders', folderDbType = 'folders';
folders;
self._account.busy = true; self._account.busy = true; // start the spinner
if (!self._account.online) { if (!self._account.online) {
// fetch list from local cache // fetch list from local cache
@ -987,15 +1138,13 @@ define(function(require) {
} else { } else {
// fetch list from imap server // fetch list from imap server
self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) {
var foldersChanged = false;
if (err) { if (err) {
done(err); done(err);
return; return;
} }
// this array is dropped directly into the ui to create the folder list // this array is dropped directly into the ui to create the folder list
folders = [ var folders = [
wellKnownFolders.inbox, wellKnownFolders.inbox,
wellKnownFolders.sent, { wellKnownFolders.sent, {
type: 'Outbox', type: 'Outbox',
@ -1005,7 +1154,9 @@ define(function(require) {
wellKnownFolders.trash wellKnownFolders.trash
]; ];
// are there any new folders? var foldersChanged = false; // indicates if are there any new/removed folders?
// check for added folders
folders.forEach(function(folder) { folders.forEach(function(folder) {
if (!_.findWhere(self._account.folders, { if (!_.findWhere(self._account.folders, {
path: folder.path path: folder.path
@ -1016,7 +1167,7 @@ define(function(require) {
} }
}); });
// have any folders been deleted? // check for deleted folders
self._account.folders.forEach(function(folder) { self._account.folders.forEach(function(folder) {
if (!_.findWhere(folders, { if (!_.findWhere(folders, {
path: folder.path path: folder.path
@ -1027,12 +1178,14 @@ define(function(require) {
} }
}); });
// if folder have changed, we need to persist them to disk.
if (!foldersChanged) { if (!foldersChanged) {
readMessagesFromDisk(); readMessagesFromDisk();
return; return;
} }
// persist encrypted list in device storage // persist encrypted list in device storage
// NB! persis the array we received from IMAP! do *not* persist self._account.folders with all the messages...
self._devicestorage.storeList([folders], folderDbType, function(err) { self._devicestorage.storeList([folders], folderDbType, function(err) {
if (err) { if (err) {
done(err); done(err);
@ -1045,6 +1198,7 @@ define(function(require) {
return; return;
} }
// fill uninitialized folders with the locally available messages
function readMessagesFromDisk() { function readMessagesFromDisk() {
if (!self._account.folders || self._account.folders.length === 0) { if (!self._account.folders || self._account.folders.length === 0) {
done(); done();
@ -1060,7 +1214,7 @@ define(function(require) {
return; return;
} }
// sync: messages on disk -> scope // sync messages from disk to the folder model
self.refreshFolder({ self.refreshFolder({
folder: folder folder: folder
}, function(err) { }, function(err) {
@ -1088,7 +1242,12 @@ define(function(require) {
// //
/** /**
* Mark imap messages as un-/read or un-/answered * Mark messages as un-/read or un-/answered on IMAP
*
* @param {Object} options.folder The folder where to find the message
* @param {Number} options.uid The uid for which to change the flags
* @param {Number} options.unread Un-/Read flag
* @param {Number} options.answered Un-/Answered flag
*/ */
EmailDAO.prototype._imapMark = function(options, callback) { EmailDAO.prototype._imapMark = function(options, callback) {
if (!this._account.online) { if (!this._account.online) {
@ -1103,6 +1262,14 @@ define(function(require) {
this._imapClient.updateFlags(options, callback); this._imapClient.updateFlags(options, callback);
}; };
/**
* If we're in the trash folder or no trash folder is available, this deletes a message from IMAP.
* Otherwise, it moves a message to the trash folder.
*
* @param {Object} options.folder The folder where to find the message
* @param {Number} options.uid The uid of the message
* @param {Function} callback(error) Callback with an error object in case something went wrong.
*/
EmailDAO.prototype._imapDeleteMessage = function(options, callback) { EmailDAO.prototype._imapDeleteMessage = function(options, callback) {
if (!this._account.online) { if (!this._account.online) {
callback({ callback({
@ -1126,6 +1293,7 @@ define(function(require) {
return; return;
} }
// move the message to the trash folder
this._imapClient.moveMessage({ this._imapClient.moveMessage({
path: options.folder.path, path: options.folder.path,
destination: trash.path, destination: trash.path,
@ -1134,7 +1302,8 @@ define(function(require) {
}; };
/** /**
* Get an email messsage without the body * Get list messsage headers without the body
*
* @param {String} options.folder The folder * @param {String} options.folder The folder
* @param {Number} options.firstUid The lower bound of the uid (inclusive) * @param {Number} options.firstUid The lower bound of the uid (inclusive)
* @param {Number} options.lastUid The upper bound of the uid range (inclusive) * @param {Number} options.lastUid The upper bound of the uid range (inclusive)
@ -1192,16 +1361,38 @@ define(function(require) {
// //
/**
* List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided),
* or "email_[FOLDER PATH]", respectively
*
* @param {Object} options.folder The folder for which to list the content
* @param {Object} options.uid A specific uid to look up locally in the folder
* @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred
*/
EmailDAO.prototype._localListMessages = function(options, callback) { EmailDAO.prototype._localListMessages = function(options, callback) {
var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : ''); var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : '');
this._devicestorage.listItems(dbType, 0, null, callback); this._devicestorage.listItems(dbType, 0, null, callback);
}; };
/**
* Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]"
*
* @param {Object} options.folder The folder for which to list the content
* @param {Array} options.messages The messages to store
* @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred
*/
EmailDAO.prototype._localStoreMessages = function(options, callback) { EmailDAO.prototype._localStoreMessages = function(options, callback) {
var dbType = 'email_' + options.folder.path; var dbType = 'email_' + options.folder.path;
this._devicestorage.storeList(options.emails, dbType, callback); this._devicestorage.storeList(options.emails, dbType, callback);
}; };
/**
* Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]"
*
* @param {Object} options.folder The folder for which to list the content
* @param {Array} options.messages The messages to store
* @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred
*/
EmailDAO.prototype._localDeleteMessage = function(options, callback) { EmailDAO.prototype._localDeleteMessage = function(options, callback) {
var path = options.folder.path, var path = options.folder.path,
uid = options.uid, uid = options.uid,
@ -1225,21 +1416,25 @@ define(function(require) {
// //
// //
/**
* Updates a folder's unread count:
* - For the outbox, that's the total number of messages,
* - For every other folder, it's the number of unread messages
*/
function updateUnreadCount(folder) { function updateUnreadCount(folder) {
var allMsgs = folder.messages.length, var allMsgs = folder.messages.length,
unreadMsgs = _.filter(folder.messages, function(msg) { unreadMsgs = _.filter(folder.messages, function(msg) {
return msg.unread; return msg.unread;
}).length; }).length;
// for the outbox, the unread count is determined by ALL the messages
// whereas for normal folders, only the unread messages matter
folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs; folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs;
} }
/** /**
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
* @param {[type]} bodyParts The bodyParts array *
* @param {[type]} type The type to look up * @param {Array} bodyParts The bodyParts array
* @param {String} type The type to look up
* @param {undefined} result Leave undefined, only used for recursion * @param {undefined} result Leave undefined, only used for recursion
*/ */
function filterBodyParts(bodyParts, type, result) { function filterBodyParts(bodyParts, type, result) {

View File

@ -1584,6 +1584,7 @@ define(function(require) {
setFlagsStub.withArgs({ setFlagsStub.withArgs({
folder: inboxFolder, folder: inboxFolder,
message: msgs[0], message: msgs[0],
localOnly: true
}).yieldsAsync(); }).yieldsAsync();
dao.onError = function(err) { dao.onError = function(err) {