mail/src/js/dao/email-dao.js

669 lines
23 KiB
JavaScript
Raw Normal View History

define(function(require) {
'use strict';
var util = require('cryptoLib/util'),
_ = require('underscore'),
str = require('js/app-config').string;
2013-08-27 13:17:06 -04:00
var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader, emailSync) {
2014-02-25 11:29:12 -05:00
this._keychain = keychain;
this._crypto = crypto;
this._devicestorage = devicestorage;
this._pgpbuilder = pgpbuilder;
2014-02-25 13:18:37 -05:00
this._mailreader = mailreader;
this._emailSync = emailSync;
};
//
// External API
//
EmailDAO.prototype.init = function(options, callback) {
var self = this,
keypair;
self._account = options.account;
self._account.busy = false;
self._account.online = false;
self._account.loggingIn = false;
// validate email address
var emailAddress = self._account.emailAddress;
if (!util.validateEmailAddress(emailAddress)) {
callback({
errMsg: 'The user email address must be specified!'
});
return;
}
// init keychain and then crypto module
initKeychain();
function initKeychain() {
2014-03-11 12:49:47 -04:00
// call getUserKeyPair to read/sync keypair with devicestorage/cloud
self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) {
if (err) {
callback(err);
return;
}
2014-03-11 12:49:47 -04:00
keypair = storedKeypair;
initEmailSync();
});
}
function initEmailSync() {
self._emailSync.init({
account: self._account
}, function(err) {
if (err) {
callback(err);
return;
}
2014-03-11 12:49:47 -04:00
initFolders();
});
}
function initFolders() {
// try init folders from memory, since imap client not initiated yet
self._imapListFolders(function(err, folders) {
// dont handle offline case this time
if (err && err.code !== 42) {
callback(err);
return;
}
self._account.folders = folders;
callback(null, keypair);
});
}
};
EmailDAO.prototype.onConnect = function(options, callback) {
var self = this;
2013-12-06 11:47:38 -05:00
self._account.loggingIn = true;
self._imapClient = options.imapClient;
self._pgpMailer = options.pgpMailer;
// notify emailSync
self._emailSync.onConnect({
imapClient: self._imapClient
}, function(err) {
if (err) {
self._account.loggingIn = false;
callback(err);
return;
}
// connect to newly created imap client
self._imapLogin(onLogin);
});
function onLogin(err) {
if (err) {
self._account.loggingIn = false;
callback(err);
return;
}
// set status to online
self._account.loggingIn = false;
self._account.online = true;
// init folders
self._imapListFolders(function(err, folders) {
if (err) {
callback(err);
return;
}
// only overwrite folders if they are not yet set
if (!self._account.folders) {
self._account.folders = folders;
}
var inbox = _.findWhere(self._account.folders, {
type: 'Inbox'
});
if (inbox) {
self._imapClient.listenForChanges({
path: inbox.path
}, function(error, path) {
if (typeof self.onNeedsSync === 'function') {
self.onNeedsSync(error, path);
}
});
}
callback();
});
}
2013-10-21 07:10:42 -04:00
};
EmailDAO.prototype.onDisconnect = function(options, callback) {
// set status to online
this._account.online = false;
this._imapClient = undefined;
this._pgpMailer = undefined;
// notify emailSync
this._emailSync.onDisconnect(null, callback);
};
EmailDAO.prototype.unlock = function(options, callback) {
2013-10-21 07:10:42 -04:00
var self = this;
if (options.keypair) {
2013-10-21 07:10:42 -04:00
// import existing key pair into crypto module
handleExistingKeypair(options.keypair);
2013-10-21 07:10:42 -04:00
return;
}
2013-10-21 07:10:42 -04:00
// no keypair for is stored for the user... generate a new one
self._crypto.generateKeys({
emailAddress: self._account.emailAddress,
keySize: self._account.asymKeySize,
passphrase: options.passphrase
2013-10-21 07:10:42 -04:00
}, function(err, generatedKeypair) {
if (err) {
callback(err);
2013-10-11 21:19:01 -04:00
return;
}
handleGenerated(generatedKeypair);
});
function handleExistingKeypair(keypair) {
var pubUserID, privUserID;
// check if key IDs match
if (!keypair.privateKey._id || keypair.privateKey._id !== keypair.publicKey._id) {
callback({
errMsg: 'Key IDs dont match!'
});
return;
}
// check if the key's user ID matches the current account
pubUserID = self._crypto.getKeyParams(keypair.publicKey.publicKey).userId;
privUserID = self._crypto.getKeyParams(keypair.privateKey.encryptedKey).userId;
if (pubUserID.indexOf(self._account.emailAddress) === -1 || privUserID.indexOf(self._account.emailAddress) === -1) {
callback({
errMsg: 'User IDs dont match!'
});
return;
}
// import existing key pair into crypto module
self._crypto.importKeys({
passphrase: options.passphrase,
privateKeyArmored: keypair.privateKey.encryptedKey,
publicKeyArmored: keypair.publicKey.publicKey
}, function(err) {
if (err) {
callback(err);
return;
}
// set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey;
callback();
});
}
function handleGenerated(generatedKeypair) {
2013-10-21 07:10:42 -04:00
// import the new key pair into crypto module
self._crypto.importKeys({
passphrase: options.passphrase,
2013-10-21 07:10:42 -04:00
privateKeyArmored: generatedKeypair.privateKeyArmored,
publicKeyArmored: generatedKeypair.publicKeyArmored
}, function(err) {
if (err) {
callback(err);
return;
}
2013-10-21 07:10:42 -04:00
// persist newly generated keypair
var newKeypair = {
publicKey: {
_id: generatedKeypair.keyId,
userId: self._account.emailAddress,
publicKey: generatedKeypair.publicKeyArmored
},
privateKey: {
_id: generatedKeypair.keyId,
userId: self._account.emailAddress,
encryptedKey: generatedKeypair.privateKeyArmored
2013-10-11 21:19:01 -04:00
}
2013-10-21 07:10:42 -04:00
};
2014-02-24 04:14:07 -05:00
self._keychain.putUserKeyPair(newKeypair, function(err) {
if (err) {
callback(err);
return;
}
// set decrypted privateKey to pgpMailer
2014-02-25 11:29:12 -05:00
self._pgpbuilder._privateKey = self._crypto._privateKey;
2014-02-24 04:14:07 -05:00
callback();
});
});
}
};
2014-02-24 04:14:07 -05:00
EmailDAO.prototype.syncOutbox = function(options, callback) {
this._emailSync.syncOutbox(options, callback);
2014-02-24 04:14:07 -05:00
};
EmailDAO.prototype.sync = function(options, callback) {
this._emailSync.sync(options, callback);
};
2013-09-26 07:26:57 -04:00
/**
* Streams message content
* @param {Object} options.message The message for which to retrieve the body
* @param {Object} options.folder The IMAP folder
* @param {Function} callback(error, message) Invoked when the message is streamed, or provides information if an error occurred
*/
2014-02-20 09:42:51 -05:00
EmailDAO.prototype.getBody = function(options, callback) {
var self = this,
message = options.message,
folder = options.folder;
2013-10-04 09:47:30 -04:00
// 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;
}
2013-09-26 07:26:57 -04:00
2014-02-17 08:31:14 -05:00
message.loadingBody = true;
/*
* 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.
*/
retrieveContent();
function retrieveContent() {
// load the local message from memory
self._emailSync._localListMessages({
folder: folder,
uid: message.uid
}, function(err, localMessages) {
if (err || localMessages.length === 0) {
done(err);
2013-09-26 07:26:57 -04:00
return;
}
var localMessage = localMessages[0];
2014-05-12 16:07:25 -04:00
// treat attachment and non-attachment body parts separately:
// we need to fetch the content for non-attachment body parts (encrypted, signed, text, 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 !== "attachment";
});
var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) {
return bodyPart.type === "attachment";
});
// do we need to fetch content from the imap server?
var needsFetch = false;
2014-05-12 16:07:25 -04:00
contentParts.forEach(function(part) {
needsFetch = (typeof part.content === 'undefined');
});
if (!needsFetch) {
// if we have all the content we need,
// we can extract the content
message.bodyParts = localMessage.bodyParts;
extractContent();
return;
}
2013-09-26 07:26:57 -04:00
// get the raw content from the imap server
self._emailSync._getBodyParts({
folder: folder,
uid: localMessage.uid,
2014-05-12 16:07:25 -04:00
bodyParts: contentParts
}, function(err, parsedBodyParts) {
if (err) {
done(err);
2014-01-18 05:42:28 -05:00
return;
}
2014-05-12 16:07:25 -04:00
// 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
self._emailSync._localStoreMessages({
folder: folder,
emails: [localMessage]
}, function(error) {
if (error) {
done(error);
return;
}
// extract the content
extractContent();
});
2013-09-26 07:26:57 -04:00
});
});
}
function extractContent() {
if (message.encrypted) {
// show the encrypted message
message.body = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0].content;
done();
return;
}
// for unencrypted messages, this is the array where the body parts are located
var root = message.bodyParts;
if (message.signed) {
var signedPart = self._emailSync.filterBodyParts(message.bodyParts, 'signed')[0];
message.message = signedPart.message;
message.signature = signedPart.signature;
// TODO check integrity
// in case of a signed message, you only want to show the signed content and ignore the rest
root = signedPart.content;
}
2014-05-13 07:13:36 -04:00
// 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 body = _.pluck(self._emailSync.filterBodyParts(root, 'text'), 'content').join('\n');
/*
* here's how the regex works:
* - any content before the PGP block will be discarded
* - "-----BEGIN PGP MESSAGE-----" must be at the beginning (and end) of a line
* - "-----END PGP MESSAGE-----" must be at the beginning (and 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 match = body.match(/^-{5}BEGIN PGP MESSAGE-{5}$[^>]*^-{5}END PGP MESSAGE-{5}$/im);
if (match) {
// show the plain text content
message.body = match[0];
// - replace the bodyParts info with an artificial bodyPart of type "encrypted"
// - _isPgpInline is only used internally to avoid trying to parse non-MIME text with the mailreader
// - set the encrypted flag so we can signal the ui that we're handling encrypted content
message.encrypted = true;
message.bodyParts = [{
type: 'encrypted',
content: match[0],
_isPgpInline: true
}];
done();
return;
}
message.attachments = self._emailSync.filterBodyParts(root, 'attachment');
2014-05-13 07:13:36 -04:00
message.body = body;
message.html = _.pluck(self._emailSync.filterBodyParts(root, 'html'), 'content').join('\n');
done();
2013-10-16 12:56:18 -04:00
}
function done(err) {
message.loadingBody = false;
callback(err, err ? undefined : message);
}
};
2014-05-12 16:07:25 -04:00
EmailDAO.prototype.getAttachment = function(options, callback) {
this._emailSync._getBodyParts({
folder: options.folder,
uid: options.uid,
bodyParts: [options.attachment]
}, function(err, parsedBodyParts) {
if (err) {
callback(err);
return;
}
options.attachment.content = parsedBodyParts[0].content;
callback(err, err ? undefined : options.attachment);
});
};
2014-02-24 04:14:07 -05:00
EmailDAO.prototype.decryptBody = function(options, callback) {
var self = this,
message = options.message;
// the message is decrypting has no body, is not encrypted or has already been decrypted
2014-02-17 08:31:14 -05:00
if (message.decryptingBody || !message.body || !message.encrypted || message.decrypted) {
return;
}
2014-02-17 08:31:14 -05:00
message.decryptingBody = true;
// get the sender's public key for signature checking
self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) {
if (err) {
done(err);
return;
}
if (!senderPublicKey) {
// this should only happen if a mail from another channel is in the inbox
showError('Public key for sender not found!');
return;
}
// get the receiver's public key to check the message signature
var encryptedNode = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0];
self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) {
if (err || !decrypted) {
2014-05-13 07:13:36 -04:00
showError(err.errMsg || err.message || 'An error occurred during the decryption.');
return;
}
// if the encrypted node contains pgp/inline, we must not parse it
// with the mailreader as it is not well-formed MIME
if (encryptedNode._isPgpInline) {
message.body = decrypted;
message.decrypted = true;
done();
return;
}
// the mailparser works on the .raw property
encryptedNode.raw = decrypted;
2014-05-13 07:13:36 -04:00
// parse the decrypted raw content in the mailparser
self._mailreader.parse({
bodyParts: [encryptedNode]
}, function(err, parsedBodyParts) {
if (err) {
showError(err.errMsg || err.message);
return;
}
// we have successfully interpreted the descrypted message,
// so let's update the views on the message parts
message.body = _.pluck(self._emailSync.filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n');
message.html = _.pluck(self._emailSync.filterBodyParts(parsedBodyParts, 'html'), 'content').join('\n');
message.attachments = _.reject(self._emailSync.filterBodyParts(parsedBodyParts, 'attachment'), function(attmt) {
// remove the pgp-signature from the attachments
return attmt.mimeType === "application/pgp-signature";
});
message.decrypted = true;
// we're done here!
done();
});
});
});
function showError(msg) {
message.body = msg;
message.decrypted = true; // display error msh in body
done();
}
function done(err) {
message.decryptingBody = false;
callback(err, err ? undefined : message);
}
};
EmailDAO.prototype.sendEncrypted = function(options, callback) {
2014-02-24 04:14:07 -05:00
var self = this;
2013-12-12 08:47:04 -05:00
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
2014-02-24 04:14:07 -05:00
// mime encode, sign, encrypt and send email via smtp
self._pgpMailer.send({
encrypt: true,
cleartextMessage: str.message + str.signature,
2014-02-24 04:14:07 -05:00
mail: options.email,
publicKeysArmored: options.email.publicKeysArmored
}, callback);
};
EmailDAO.prototype.sendPlaintext = function(options, callback) {
2013-12-12 08:47:04 -05:00
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
// mime encode, sign and send email via smtp
this._pgpMailer.send({
mail: options.email
}, callback);
};
2014-02-24 04:14:07 -05:00
EmailDAO.prototype.encrypt = function(options, callback) {
2014-02-25 11:29:12 -05:00
this._pgpbuilder.encrypt(options, callback);
2014-02-24 04:14:07 -05:00
};
//
// Internal API
//
// IMAP API
/**
* Login the imap client
*/
EmailDAO.prototype._imapLogin = function(callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
// login IMAP client if existent
this._imapClient.login(callback);
};
/**
* Cleanup by logging the user off.
*/
EmailDAO.prototype._imapLogout = function(callback) {
2013-12-12 08:47:04 -05:00
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.logout(callback);
};
/**
* List the folders in the user's IMAP mailbox.
*/
EmailDAO.prototype._imapListFolders = function(callback) {
var self = this,
dbType = 'folders';
// check local cache
self._devicestorage.listItems(dbType, 0, null, function(err, stored) {
if (err) {
callback(err);
return;
}
if (!stored || stored.length < 1) {
// no folders cached... fetch from server
fetchFromServer();
return;
}
callback(null, stored[0]);
});
function fetchFromServer() {
var folders;
2013-12-12 08:47:04 -05:00
if (!self._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
// fetch list from imap server
self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) {
if (err) {
callback(err);
return;
}
folders = [
wellKnownFolders.inbox,
wellKnownFolders.sent, {
type: 'Outbox',
path: 'OUTBOX'
},
wellKnownFolders.drafts,
wellKnownFolders.trash
];
// cache locally
// persist encrypted list in device storage
self._devicestorage.storeList([folders], dbType, function(err) {
if (err) {
callback(err);
return;
}
callback(null, folders);
});
});
}
};
return EmailDAO;
});