2013-08-19 15:13:32 -04:00
define ( function ( require ) {
2013-08-16 14:50:47 -04:00
'use strict' ;
2013-08-19 15:13:32 -04:00
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 ) {
2013-08-16 14:50:47 -04:00
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 ;
2013-08-22 10:18:48 -04:00
self . _account = account ;
2013-08-16 14:50:47 -04:00
// validate email address
2013-08-22 10:18:48 -04:00
var emailAddress = self . _account . emailAddress ;
2013-08-16 14:50:47 -04:00
if ( ! validateEmail ( emailAddress ) ) {
callback ( {
errMsg : 'The user email address must be specified!'
} ) ;
return ;
}
// login IMAP client if existent
if ( self . _imapClient ) {
2013-08-19 15:13:32 -04:00
self . _imapClient . login ( function ( err ) {
if ( err ) {
callback ( err ) ;
return ;
}
2013-08-16 14:50:47 -04:00
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 ,
2013-08-22 10:18:48 -04:00
keySize : self . _account . symKeySize ,
rsaKeySize : self . _account . asymKeySize ,
2013-08-16 14:50:47 -04:00
storedKeypair : storedKeypair
} , function ( err , generatedKeypair ) {
if ( err ) {
callback ( err ) ;
return ;
}
if ( generatedKeypair ) {
// persist newly generated keypair
self . _keychain . putUserKeyPair ( generatedKeypair , callback ) ;
} else {
callback ( ) ;
}
} ) ;
}
} ;
2013-08-20 07:30:35 -04:00
//
2013-08-22 10:18:48 -04:00
// IMAP/SMTP Apis
2013-08-20 07:30:35 -04:00
//
/ * *
* 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 ) {
2013-08-27 13:04:26 -04:00
var self = this ,
invalidRecipient ;
2013-08-20 07:30:35 -04:00
// validate the email input
if ( ! email . to || ! email . from || ! email . to [ 0 ] . address || ! email . from [ 0 ] . address ) {
callback ( {
errMsg : 'Invalid email object!'
} ) ;
return ;
}
2013-08-27 12:23:27 -04:00
// 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
2013-08-27 13:04:26 -04:00
self . _keychain . getReveiverPublicKey ( email . to [ 0 ] . address , function ( err , receiverPubkey ) {
2013-08-27 12:23:27 -04:00
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' ;
2013-08-27 13:04:26 -04:00
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' ;
2013-08-27 12:23:27 -04:00
var PREFIX = '-----BEGIN ENCRYPTED MESSAGE-----\n' ;
var SUFFIX = '\n-----END ENCRYPTED MESSAGE-----' ;
2013-08-27 13:04:26 -04:00
var SIGNATURE = '\n\n\nSent from whiteout mail, for easy end-to-end encrypted messaging\nhttp://whiteout.io\n\n' ;
2013-08-27 12:23:27 -04:00
// 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 ) ;
}
2013-08-20 07:30:35 -04:00
} ;
/ * *
* List the folders in the user ' s IMAP mailbox .
* /
EmailDAO . prototype . imapListFolders = function ( callback ) {
2013-08-20 11:22:08 -04:00
var self = this ;
self . _imapClient . listFolders ( callback ) ;
2013-08-20 07:30:35 -04:00
} ;
/ * *
* List messages from an imap folder . This will not yet fetch the email body .
* @ param { String } options . folderName The name of the imap folder .
2013-08-20 13:48:49 -04:00
* @ 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 )
2013-08-20 07:30:35 -04:00
* /
EmailDAO . prototype . imapListMessages = function ( options , callback ) {
2013-08-20 13:48:49 -04:00
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 ) ;
2013-08-20 07:30:35 -04:00
} ;
/ * *
* Get an email messsage including the email body from imap
* @ param { String } options . messageId The
* /
EmailDAO . prototype . imapGetMessage = function ( options , callback ) {
2013-08-21 07:43:19 -04:00
var self = this ,
2013-08-21 10:07:59 -04:00
expectedItems ,
itemCounter = 0 ,
message , attachments = [ ] ;
2013-08-21 07:43:19 -04:00
// validate options
if ( ! options . folder || ! options . uid ) {
callback ( {
errMsg : 'Invalid options!'
} ) ;
return ;
}
2013-08-22 10:18:48 -04:00
// 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 */
2013-08-21 07:43:19 -04:00
function messageReady ( err , gottenMessage ) {
message = gottenMessage ;
2013-08-21 10:07:59 -04:00
itemCounter ++ ;
// remember how many items should be fetched before the callback fires
expectedItems = ( message . attachments instanceof Array ) ? message . attachments . length + 1 : 1 ;
check ( ) ;
2013-08-21 07:43:19 -04:00
}
2013-08-21 10:07:59 -04:00
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 ;
}
2013-08-22 10:18:48 -04:00
// overwrite attachments array with the uint8array variant
2013-08-21 10:07:59 -04:00
message . attachments = ( attachments . length > 0 ) ? attachments : undefined ;
2013-08-22 10:18:48 -04:00
// cache message object in memory
self . cacheItem ( options . folder , message ) ;
2013-08-21 07:43:19 -04:00
callback ( null , message ) ;
}
self . _imapClient . getMessage ( {
path : options . folder ,
uid : options . uid
} , messageReady , attachmentReady ) ;
2013-08-20 07:30:35 -04:00
} ;
2013-08-22 10:18:48 -04:00
/ * *
* 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 ;
} ;
2013-08-20 07:30:35 -04:00
2013-08-16 14:50:47 -04:00
/ * *
2013-08-22 10:18:48 -04:00
* Fetch an item from the cache with the following id
2013-08-16 14:50:47 -04:00
* /
2013-08-22 10:18:48 -04:00
EmailDAO . prototype . readCache = function ( folderName , itemId ) {
2013-08-16 14:50:47 -04:00
var self = this ;
2013-08-22 10:18:48 -04:00
// check if account has a folders attribute
if ( ! self . _account . folders ) {
2013-08-23 05:04:22 -04:00
return ;
2013-08-22 10:18:48 -04:00
}
// check folder
if ( ! self . _account . folders [ folderName ] ) {
2013-08-23 05:04:22 -04:00
return ;
2013-08-22 10:18:48 -04:00
}
return self . _account . folders [ folderName ] [ itemId ] ;
2013-08-16 14:50:47 -04:00
} ;
2013-08-22 10:18:48 -04:00
//
// Cloud storage Apis
//
2013-08-16 14:50:47 -04:00
/ * *
* 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 ;
2013-08-22 10:18:48 -04:00
// check if items are in memory already (_account.folders model)
folder = self . _account . get ( 'folders' ) . where ( {
2013-08-16 14:50:47 -04:00
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 ) ;
2013-08-22 10:18:48 -04:00
self . _account . get ( 'folders' ) . add ( folder ) ;
2013-08-16 14:50:47 -04:00
}
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
2013-08-22 10:18:48 -04:00
self . _cloudstorage . listEncryptedItems ( 'email' , self . _account . get ( 'emailAddress' ) , folderName + filter , function ( err , encryptedList ) {
2013-08-16 14:50:47 -04:00
// 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 ( ) {
2013-08-22 10:18:48 -04:00
// remove cached folder in _account model
folder = self . _account . get ( 'folders' ) . where ( {
2013-08-16 14:50:47 -04:00
name : folderName
} ) [ 0 ] ;
if ( folder ) {
2013-08-22 10:18:48 -04:00
self . _account . get ( 'folders' ) . remove ( folder ) ;
2013-08-16 14:50:47 -04:00
}
callback ( ) ;
} ) ;
} ) ;
} ) ;
}
} ;
/ * *
* Send a plaintext Email to the user ' s outbox in the cloud
* /
EmailDAO . prototype . sendEmail = function ( email , callback ) {
var self = this ,
2013-08-22 10:18:48 -04:00
userId = self . _account . get ( 'emailAddress' ) ;
2013-08-16 14:50:47 -04:00
// 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 ) {
2013-08-20 07:30:35 -04:00
// send email via cloud service
self . _cloudstorage . deliverEmail ( email , userId , recipient , function ( err ) {
callback ( err ) ;
} ) ;
2013-08-16 14:50:47 -04:00
}
} ;
//
// helper functions
//
2013-08-20 07:30:35 -04:00
/ * *
* Validates an email address
* /
2013-08-16 14:50:47 -04:00
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 ;
2013-06-10 17:50:26 -04:00
} ) ;