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

583 lines
18 KiB
JavaScript
Raw Normal View History

define(function(require) {
'use strict';
var _ = require('underscore'),
util = require('cryptoLib/util'),
crypto = require('js/crypto/crypto'),
jsonDB = require('js/dao/lawnchair-dao'),
devicestorage = require('js/dao/devicestorage-dao'),
app = require('js/app-config');
/**
* A high-level Data-Access Api for handling Email synchronization
* between the cloud service and the device's local storage
*/
var EmailDAO = function(keychain, imapClient, smtpClient) {
var self = this;
self._keychain = keychain;
self._imapClient = imapClient;
self._smtpClient = smtpClient;
};
/**
* Inits all dependencies
*/
EmailDAO.prototype.init = function(account, password, callback) {
var self = this;
self._account = account;
// validate email address
var emailAddress = self._account.emailAddress;
if (!validateEmail(emailAddress)) {
callback({
errMsg: 'The user email address must be specified!'
});
return;
}
// login IMAP client if existent
if (self._imapClient) {
self._imapClient.login(function(err) {
if (err) {
callback(err);
return;
}
initKeychain();
});
} else {
initKeychain();
}
function initKeychain() {
// init user's local database
jsonDB.init(emailAddress);
// call getUserKeyPair to read/sync keypair with devicestorage/cloud
self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) {
if (err) {
callback(err);
return;
}
// init crypto
initCrypto(storedKeypair);
});
}
function initCrypto(storedKeypair) {
crypto.init({
emailAddress: emailAddress,
password: password,
keySize: self._account.symKeySize,
rsaKeySize: self._account.asymKeySize,
storedKeypair: storedKeypair
}, function(err, generatedKeypair) {
if (err) {
callback(err);
return;
}
if (generatedKeypair) {
// persist newly generated keypair
self._keychain.putUserKeyPair(generatedKeypair, callback);
} else {
callback();
}
});
}
};
//
// IMAP/SMTP Apis
//
/**
* Cleanup by logging the user off.
*/
EmailDAO.prototype.destroy = function(callback) {
var self = this;
self._imapClient.logout(callback);
};
/**
* Send an email client side via STMP.
*/
EmailDAO.prototype.smtpSend = function(email, callback) {
var self = this,
invalidRecipient;
// validate the email input
if (!email.to || !email.from || !email.to[0].address || !email.from[0].address) {
callback({
errMsg: 'Invalid email object!'
});
return;
}
// validate email addresses
_.each(email.to, function(i) {
if (!validateEmail(i.address)) {
invalidRecipient = i.address;
}
});
if (invalidRecipient) {
callback({
errMsg: 'Invalid recipient: ' + invalidRecipient
});
return;
}
if (!validateEmail(email.from[0].address)) {
callback({
errMsg: 'Invalid sender: ' + email.from
});
return;
}
// generate a new UUID for the new email
email.id = util.UUID();
// only support single recipient for e-2-e encryption
// check if receiver has a public key
self._keychain.getReveiverPublicKey(email.to[0].address, function(err, receiverPubkey) {
if (err) {
callback(err);
return;
}
// validate public key
if (!receiverPubkey) {
callback({
errMsg: 'No public key found for: ' + email.from
});
return;
}
// public key found... encrypt and send
encrypt(email, receiverPubkey);
});
function encrypt(email, receiverPubkey) {
var ptItems = [email],
receiverPubkeys = [receiverPubkey],
from, to;
to = (email.to[0].name || email.to[0].address).split('@')[0].split('.')[0].split(' ')[0];
from = email.from[0].name || email.from[0].address;
var NEW_SUBJECT = '[whiteout] Encrypted message';
var MESSAGE = 'Hi ' + to + ',\n\nthis is a private conversation just between the two of us. To read the encrypted message below, simply install Whiteout Mail for Chrome and encrypt your emails without any hassle: https://chrome.google.com/webstore/detail/whiteout-mail/jjgghafhamholjigjoghcfcekhkonijg\n\n\n';
var PREFIX = '-----BEGIN ENCRYPTED MESSAGE-----\n';
var SUFFIX = '\n-----END ENCRYPTED MESSAGE-----';
var SIGNATURE = '\n\n\nSent from whiteout mail, for easy end-to-end encrypted messaging\nhttp://whiteout.io\n\n';
// encrypt the email
crypto.encryptListForUser(ptItems, receiverPubkeys, function(err, encryptedList) {
if (err) {
callback(err);
return;
}
// build message envelope
var ct = btoa(JSON.stringify(encryptedList[0]));
email.body = MESSAGE + PREFIX + ct + SUFFIX + SIGNATURE;
email.subject = NEW_SUBJECT;
send(email);
});
}
function send(email) {
self._smtpClient.send(email, callback);
}
};
/**
* List the folders in the user's IMAP mailbox.
*/
EmailDAO.prototype.imapListFolders = function(callback) {
var self = this;
self._imapClient.listFolders(callback);
};
/**
* List messages from an imap folder. This will not yet fetch the email body.
* @param {String} options.folderName The name of the imap folder.
* @param {Number} options.offset The offset of items to fetch (0 is the last stored item)
* @param {Number} options.num The number of items to fetch (null means fetch all)
*/
EmailDAO.prototype.imapListMessages = function(options, callback) {
var self = this;
// validate options
if (!options.folder || typeof options.offset === 'undefined' || typeof options.num === 'undefined') {
callback({
errMsg: 'Invalid options!'
});
return;
}
self._imapClient.listMessages({
path: options.folder,
offset: options.offset,
length: options.num
}, callback);
};
/**
* Get an email messsage including the email body from imap
* @param {String} options.messageId The
*/
EmailDAO.prototype.imapGetMessage = function(options, callback) {
var self = this,
expectedItems,
itemCounter = 0,
message, attachments = [];
// validate options
if (!options.folder || !options.uid) {
callback({
errMsg: 'Invalid options!'
});
return;
}
// try fetching from cache before doing a roundtrip
message = self.readCache(options.folder, options.uid);
if (message) {
// message was fetched from cache successfully
callback(null, message);
return;
}
/* message was not found in cache... fetch from imap server */
function messageReady(err, gottenMessage) {
message = gottenMessage;
itemCounter++;
// remember how many items should be fetched before the callback fires
expectedItems = (message.attachments instanceof Array) ? message.attachments.length + 1 : 1;
check();
}
function attachmentReady(err, gottenAttachment) {
attachments.push(gottenAttachment);
itemCounter++;
check();
}
function check() {
// go for another round you don't yet know how mich to fetch or you haven't fetch enough
if (!expectedItems || itemCounter < expectedItems) {
return;
}
// overwrite attachments array with the uint8array variant
message.attachments = (attachments.length > 0) ? attachments : undefined;
// cache message object in memory
self.cacheItem(options.folder, message);
callback(null, message);
}
self._imapClient.getMessage({
path: options.folder,
uid: options.uid
}, messageReady, attachmentReady);
};
/**
* Checks if an item is already cached and if not, cache it.
*/
EmailDAO.prototype.cacheItem = function(folderName, item) {
var self = this;
// check if account has a folders attribute
if (!self._account.folders) {
self._account.folders = {};
}
// create folder if not existant
if (!self._account.folders[folderName]) {
self._account.folders[folderName] = {};
}
// cache item
self._account.folders[folderName][item.uid] = item;
};
/**
* Fetch an item from the cache with the following id
*/
EmailDAO.prototype.readCache = function(folderName, itemId) {
var self = this;
// check if account has a folders attribute
if (!self._account.folders) {
2013-08-23 05:04:22 -04:00
return;
}
// check folder
if (!self._account.folders[folderName]) {
2013-08-23 05:04:22 -04:00
return;
}
return self._account.folders[folderName][itemId];
};
//
// Cloud storage Apis
//
/**
* Fetch a list of emails from the device's local storage
* @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)
*/
EmailDAO.prototype.listItems = function(folderName, offset, num, callback) {
var self = this,
collection, folder;
// check if items are in memory already (_account.folders model)
folder = self._account.get('folders').where({
name: folderName
})[0];
if (!folder) {
// get encrypted items from storage
devicestorage.listEncryptedItems('email_' + folderName, offset, num, function(err, encryptedList) {
if (err) {
callback(err);
return;
}
if (encryptedList.length === 0) {
callback(null, []);
return;
}
// decrypt list
crypto.decryptKeysAndList(encryptedList, function(err, decryptedList) {
if (err) {
callback(err);
return;
}
// cache collection in folder memory
if (decryptedList.length > 0) {
folder = new app.model.Folder({
name: folderName
});
folder.set('items', decryptedList);
self._account.get('folders').add(folder);
}
callback(null, decryptedList);
});
});
} else {
// read items from memory
collection = folder.get('items');
callback(null, collection);
}
};
/**
* Synchronize a folder's items from the cloud to the device-storage
* @param folderName [String] The name of the folder e.g. 'inbox'
*/
EmailDAO.prototype.syncFromCloud = function(folderName, callback) {
var self = this,
folder, already, pubkeyIds = [];
// fetch most recent date
this.listItems(folderName, 0, 1, function(err, localItems) {
if (err) {
callback(err); // error
return;
}
var filter = '';
if (localItems && localItems.length > 0) {
// get gmt date since that's what the storage service seems to use
var sentDate = localItems[localItems.length - 1].sentDate;
var date = util.parseDate(sentDate);
date.setHours(date.getHours() + (date.getTimezoneOffset() / 60));
var gmtDate = util.formatDate(date);
// sync delta of last item sent date
filter = '?date=' + gmtDate;
startSync(filter);
} else {
// do a full sync of all items on the cloud
startSync(filter);
}
});
function startSync(filter) {
// fetch items from the cloud
self._cloudstorage.listEncryptedItems('email', self._account.get('emailAddress'), folderName + filter, function(err, encryptedList) {
// return if an error occured
if (err) {
callback({
errMsg: 'Syncing encrypted items from cloud failed!',
err: err
}); // error
return;
}
if (encryptedList.length === 0) {
callback();
return;
}
// TODO: remove old folder items from devicestorage
reencryptItems(encryptedList);
});
}
function reencryptItems(encryptedList) {
// gather public key ids required to verify signatures
encryptedList.forEach(function(i) {
already = null;
already = _.findWhere(pubkeyIds, {
_id: i.senderPk
});
if (!already) {
pubkeyIds.push({
_id: i.senderPk
});
}
});
// fetch public keys from keychain
self._keychain.getPublicKeys(pubkeyIds, function(err, senderPubkeys) {
if (err) {
callback(err);
return;
}
// verfiy signatures and re-encrypt item keys
crypto.reencryptListKeysForUser(encryptedList, senderPubkeys, function(err, encryptedKeyList) {
if (err) {
callback(err);
return;
}
// persist encrypted list in device storage
devicestorage.storeEcryptedList(encryptedKeyList, 'email_' + folderName, function() {
// remove cached folder in _account model
folder = self._account.get('folders').where({
name: folderName
})[0];
if (folder) {
self._account.get('folders').remove(folder);
}
callback();
});
});
});
}
};
/**
* Send a plaintext Email to the user's outbox in the cloud
*/
EmailDAO.prototype.sendEmail = function(email, callback) {
var self = this,
userId = self._account.get('emailAddress');
// validate email addresses
var invalidRecipient;
_.each(email.to, function(i) {
if (!validateEmail(i.address)) {
invalidRecipient = i.address;
}
});
if (invalidRecipient) {
callback({
errMsg: 'Invalid recipient: ' + invalidRecipient
});
return;
}
if (!validateEmail(email.from[0].address)) {
callback({
errMsg: 'Invalid sender: ' + email.from
});
return;
}
// generate a new UUID for the new email
email.id = util.UUID();
// set sent date
email.sentDate = util.formatDate(new Date());
// only support single recipient for e-2-e encryption
var recipient = email.to[0].address;
// check if receiver has a public key
self._keychain.getReveiverPublicKey(recipient, function(err, receiverPubkey) {
if (err) {
callback(err);
return;
}
if (receiverPubkey) {
// public key found... encrypt and send
encrypt(email, receiverPubkey);
} else {
// no public key found... send plaintext mail via SMTP
send(email);
}
});
function encrypt(email, receiverPubkey) {
// encrypt the email
crypto.encryptListForUser([email], [receiverPubkey], function(err, encryptedList) {
if (err) {
callback(err);
return;
}
var ct = encryptedList[0];
var envelope = {
id: email.id,
crypto: 'rsa-1024-sha-256-aes-128-cbc',
sentDate: email.sentDate,
ciphertext: ct.ciphertext,
encryptedKey: ct.encryptedKey,
iv: ct.iv,
signature: ct.signature,
senderPk: ct.senderPk
};
send(envelope);
});
}
function send(email) {
// send email via cloud service
self._cloudstorage.deliverEmail(email, userId, recipient, function(err) {
callback(err);
});
}
};
//
// helper functions
//
/**
* Validates an email address
*/
function validateEmail(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
return EmailDAO;
});