define(function(require) { 'use strict'; var _ = require('underscore'), util = require('cryptoLib/util'), str = require('js/app-config').string, consts = require('js/app-config').constants; /** * 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, crypto, devicestorage) { var self = this; self._keychain = keychain; self._imapClient = imapClient; self._smtpClient = smtpClient; self._crypto = crypto; self._devicestorage = devicestorage; // delegation-esque pattern to mitigate between node-style events and plain js self._imapClient.onIncomingMessage = function(message) { if (typeof self.onIncomingMessage === 'function') { self.onIncomingMessage(message); } }; }; /** * Inits all dependencies */ EmailDAO.prototype.init = function(account, callback) { var self = this; self._account = account; // 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() { // init user's local database self._devicestorage.init(emailAddress, function() { // call getUserKeyPair to read/sync keypair with devicestorage/cloud self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { if (err) { callback(err); return; } callback(null, storedKeypair); }); }); } }; EmailDAO.prototype.unlock = function(keypair, passphrase, callback) { var self = this; if (keypair && keypair.privateKey && keypair.publicKey) { // import existing key pair into crypto module self._crypto.importKeys({ passphrase: passphrase, privateKeyArmored: keypair.privateKey.encryptedKey, publicKeyArmored: keypair.publicKey.publicKey }, callback); return; } // no keypair for is stored for the user... generate a new one self._crypto.generateKeys({ emailAddress: self._account.emailAddress, keySize: self._account.asymKeySize, passphrase: passphrase }, function(err, generatedKeypair) { if (err) { callback(err); return; } // import the new key pair into crypto module self._crypto.importKeys({ passphrase: passphrase, privateKeyArmored: generatedKeypair.privateKeyArmored, publicKeyArmored: generatedKeypair.publicKeyArmored }, function(err) { if (err) { callback(err); return; } // 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 } }; self._keychain.putUserKeyPair(newKeypair, callback); }); }); }; // // IMAP Apis // /** * Login the imap client */ EmailDAO.prototype.imapLogin = function(callback) { var self = this; // login IMAP client if existent self._imapClient.login(callback); }; /** * Cleanup by logging the user off. */ EmailDAO.prototype.destroy = function(callback) { var self = this; self._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; // 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); }); }); } }; /** * Get the number of unread message for a folder */ EmailDAO.prototype.unreadMessages = function(path, callback) { var self = this; self._imapClient.unreadMessages(path, callback); }; /** * Fetch a list of emails from the device's local storage */ EmailDAO.prototype.listMessages = function(options, callback) { var self = this, cleartextList = []; // validate options if (!options.folder) { callback({ errMsg: 'Invalid options!' }); return; } options.offset = (typeof options.offset === 'undefined') ? 0 : options.offset; options.num = (typeof options.num === 'undefined') ? null : options.num; // fetch items from device storage self._devicestorage.listItems('email_' + options.folder, options.offset, options.num, function(err, emails) { if (err) { callback(err); return; } if (emails.length === 0) { callback(null, cleartextList); return; } var after = _.after(emails.length, function() { callback(null, cleartextList); }); _.each(emails, function(email) { handleMail(email, after); }); }); function handleMail(email, localCallback) { // remove subject filter prefix email.subject = email.subject.split(str.subjectPrefix)[1]; // encrypted mail if (isPGPMail(email)) { email = parseMessageBlock(email); decrypt(email, localCallback); return; } // verification mail if (isVerificationMail(email)) { verify(email, localCallback); return; } // cleartext mail cleartextList.push(email); localCallback(); } function isPGPMail(email) { return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1; } function isVerificationMail(email) { return email.subject === consts.verificationSubject; } function parseMessageBlock(email) { var messageBlock; // parse email body for encrypted message block // get ascii armored message block by prefix and suffix messageBlock = email.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0]; // add prefix and suffix again email.body = str.cryptPrefix + messageBlock + str.cryptSuffix; return email; } function decrypt(email, localCallback) { // fetch public key required to verify signatures var sender = email.from[0].address; self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) { if (err) { callback(err); return; } // decrypt and verfiy signatures self._crypto.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) { if (err) { callback(err); return; } email.body = decrypted; cleartextList.push(email); localCallback(); }); }); } function verify(email, localCallback) { var uuid, index; if (!email.unread) { // don't bother if the email was already marked as read localCallback(); return; } index = email.body.indexOf(consts.verificationUrlPrefix); if (index === -1) { localCallback(); return; } uuid = email.body.substr(index + consts.verificationUrlPrefix.length, consts.verificationUuidLength); self._keychain.verifyPublicKey(uuid, function(err) { if (err) { console.error('Unable to verify public key: ' + err.errMsg); localCallback(); return; } // public key has been verified, mark the message as read, delete it, and ignore it in the future self.imapMarkMessageRead({ folder: options.folder, uid: email.uid }, function(err) { if (err) { // if marking the mail as read failed, don't bother localCallback(); return; } self.imapDeleteMessage({ folder: options.folder, uid: email.uid }, function() { localCallback(); }); }); }); } }; /** * High level sync operation for the delta from the user's IMAP inbox */ EmailDAO.prototype.imapSync = function(options, callback) { var self = this, dbType = 'email_' + options.folder; fetchList(function(err, emails) { if (err) { callback(err); return; } // delete old items from db self._devicestorage.removeList(dbType, function(err) { if (err) { callback(err); return; } // persist encrypted list in device storage self._devicestorage.storeList(emails, dbType, callback); }); }); function fetchList(callback) { var headers = []; // fetch imap folder's message list self.imapListMessages({ folder: options.folder, offset: options.offset, num: options.num }, function(err, emails) { if (err) { callback(err); return; } // find encrypted messages by subject emails.forEach(function(i) { if (typeof i.subject === 'string' && i.subject.indexOf(str.subjectPrefix) !== -1) { headers.push(i); } }); // fetch message bodies fetchBodies(headers, callback); }); } function fetchBodies(headers, callback) { var emails = []; if (headers.length < 1) { callback(null, emails); return; } var after = _.after(headers.length, function() { callback(null, emails); }); _.each(headers, function(header) { self.imapGetMessage({ folder: options.folder, uid: header.uid }, function(err, message) { if (err) { callback(err); return; } // set gotten attributes like body to message object containing list meta data like 'unread' or 'replied' header.id = message.id; header.body = message.body; header.html = message.html; header.attachments = message.attachments; emails.push(header); after(); }); }); } }; /** * 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; 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; self._imapClient.getMessagePreview({ path: options.folder, uid: options.uid }, callback); }; EmailDAO.prototype.imapMoveMessage = function(options, callback) { var self = this; self._imapClient.moveMessage({ path: options.folder, uid: options.uid, destination: options.destination }, moved); function moved(err) { if (err) { callback(err); return; } // delete from local db self._devicestorage.removeList('email_' + options.folder + '_' + options.uid, callback); } }; EmailDAO.prototype.imapDeleteMessage = function(options, callback) { var self = this; self._imapClient.deleteMessage({ path: options.folder, uid: options.uid }, moved); function moved(err) { if (err) { callback(err); return; } // delete from local db self._devicestorage.removeList('email_' + options.folder + '_' + options.uid, callback); } }; EmailDAO.prototype.imapMarkMessageRead = function(options, callback) { var self = this; self._imapClient.updateFlags({ path: options.folder, uid: options.uid, unread: false }, callback); }; // // SMTP Apis // /** * 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 (!util.validateEmailAddress(i.address)) { invalidRecipient = i.address; } }); if (invalidRecipient) { callback({ errMsg: 'Invalid recipient: ' + invalidRecipient }); return; } if (!util.validateEmailAddress(email.from[0].address)) { callback({ errMsg: 'Invalid sender: ' + email.from }); return; } // only support single recipient for e-2-e encryption // check if receiver has a public key self._keychain.getReceiverPublicKey(email.to[0].address, function(err, receiverPubkey) { if (err) { callback(err); return; } // validate public key if (!receiverPubkey) { callback({ errMsg: 'User has no public key yet!' }); // user hasn't registered a public key yet... invite //self.encryptForNewUser(email, callback); return; } // public key found... encrypt and send self.encryptForUser(email, receiverPubkey.publicKey, callback); }); }; /** * Encrypt an email asymmetrically for an exisiting user with their public key */ EmailDAO.prototype.encryptForUser = function(email, receiverPubkey, callback) { var self = this, pt = email.body, receiverPubkeys = [receiverPubkey]; // get own public key so send message can be read self._crypto.exportKeys(function(err, ownKeys) { if (err) { callback(err); return; } // add own public key to receiver list receiverPubkeys.push(ownKeys.publicKeyArmored); // encrypt the email self._crypto.encrypt(pt, receiverPubkeys, function(err, ct) { if (err) { callback(err); return; } // bundle encrypted email together for sending frameEncryptedMessage(email, ct); self.send(email, callback); }); }); }; /** * Frames an encrypted message in base64 Format. */ function frameEncryptedMessage(email, ct) { var to, greeting; var MESSAGE = str.message + '\n\n\n', SIGNATURE = '\n\n\n' + str.signature + '\n' + str.webSite + '\n\n'; // get first name of recipient to = (email.to[0].name || email.to[0].address).split('@')[0].split('.')[0].split(' ')[0]; greeting = 'Hi ' + to + ',\n\n'; // build encrypted text body email.body = greeting + MESSAGE + ct + SIGNATURE; email.subject = str.subjectPrefix + email.subject; return email; } /** * Send an actual message object via smtp */ EmailDAO.prototype.send = function(email, callback) { var self = this; self._smtpClient.send(email, callback); }; return EmailDAO; });