2013-08-19 15:13:32 -04:00
define ( function ( require ) {
2013-08-16 14:50:47 -04:00
'use strict' ;
2014-06-05 09:26:19 -04:00
var util = require ( 'js/crypto/util' ) ,
2013-12-03 13:21:50 -05:00
_ = require ( 'underscore' ) ,
2014-05-23 08:23:50 -04:00
config = require ( 'js/app-config' ) . config ,
2014-03-11 13:57:03 -04:00
str = require ( 'js/app-config' ) . string ;
2013-08-27 13:17:06 -04:00
2014-07-31 03:57:57 -04:00
//
//
// Constants
//
//
var FOLDER _DB _TYPE = 'folders' ;
var SYNC _TYPE _NEW = 'new' ;
var SYNC _TYPE _DELETED = 'deleted' ;
var SYNC _TYPE _MSGS = 'messages' ;
var FOLDER _TYPE _INBOX = 'Inbox' ;
var FOLDER _TYPE _SENT = 'Sent' ;
var FOLDER _TYPE _DRAFTS = 'Drafts' ;
var FOLDER _TYPE _TRASH = 'Trash' ;
var MSG _ATTR _UID = 'uid' ;
var MSG _PART _ATTR _CONTENT = 'content' ;
var MSG _PART _TYPE _ATTACHMENT = 'attachment' ;
var MSG _PART _TYPE _ENCRYPTED = 'encrypted' ;
var MSG _PART _TYPE _SIGNED = 'signed' ;
var MSG _PART _TYPE _TEXT = 'text' ;
var MSG _PART _TYPE _HTML = 'html' ;
//
//
// Email Dao
//
//
2014-06-03 06:15:41 -04:00
/ * *
* High - level data access object that orchestrates everything around the handling of encrypted mails :
* PGP de - / e n c r y p t i o n , r e c e i v i n g v i a I M A P , s e n d i n g v i a S M T P , M I M E p a r s i n g , l o c a l d b p e r s i s t e n c e
*
* @ param { Object } keychain The keychain DAO handles keys transparently
2014-06-13 06:33:30 -04:00
* @ param { Object } pgp Orchestrates decryption
2014-06-03 06:15:41 -04:00
* @ 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
* /
2014-06-13 06:33:30 -04:00
var EmailDAO = function ( keychain , pgp , devicestorage , pgpbuilder , mailreader ) {
2014-02-25 11:29:12 -05:00
this . _keychain = keychain ;
2014-06-13 06:33:30 -04:00
this . _pgp = pgp ;
2014-02-25 11:29:12 -05:00
this . _devicestorage = devicestorage ;
this . _pgpbuilder = pgpbuilder ;
2014-02-25 13:18:37 -05:00
this . _mailreader = mailreader ;
2013-08-16 14:50:47 -04:00
} ;
2014-05-23 08:23:50 -04:00
//
2013-12-03 13:21:50 -05:00
//
2014-05-23 08:23:50 -04:00
// Public API
2013-12-03 13:21:50 -05:00
//
2014-05-23 08:23:50 -04:00
//
2013-12-03 13:21:50 -05:00
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2013-12-03 13:21:50 -05:00
EmailDAO . prototype . init = function ( options , callback ) {
var self = this ,
keypair ;
2013-08-16 14:50:47 -04:00
2013-12-03 13:21:50 -05:00
self . _account = options . account ;
2014-07-11 09:03:03 -04:00
self . _account . busy = 0 ; // triggers the spinner
2013-12-09 13:21:52 -05:00
self . _account . online = false ;
2014-05-07 13:19:51 -04:00
self . _account . loggingIn = false ;
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-30 05:42:32 -04:00
if ( ! util . validateEmailAddress ( emailAddress ) ) {
2013-08-16 14:50:47 -04:00
callback ( {
errMsg : 'The user email address must be specified!'
} ) ;
return ;
}
2013-09-26 11:37:56 -04:00
// init keychain and then crypto module
initKeychain ( ) ;
2013-08-16 14:50:47 -04:00
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 ;
}
2013-12-03 13:21:50 -05:00
2014-03-11 12:49:47 -04:00
keypair = storedKeypair ;
initFolders ( ) ;
2013-12-03 13:21:50 -05:00
} ) ;
}
function initFolders ( ) {
2013-12-09 13:21:52 -05:00
// try init folders from memory, since imap client not initiated yet
2014-06-23 07:03:56 -04:00
self . _initFoldersFromDisk ( function ( err ) {
2013-12-10 17:05:17 -05:00
// dont handle offline case this time
if ( err && err . code !== 42 ) {
2013-12-03 13:21:50 -05:00
callback ( err ) ;
return ;
}
2013-12-09 13:21:52 -05:00
callback ( null , keypair ) ;
} ) ;
}
} ;
2013-12-03 13:21:50 -05:00
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2013-12-03 13:21:50 -05:00
EmailDAO . prototype . unlock = function ( options , callback ) {
2013-10-21 07:10:42 -04:00
var self = this ;
2013-10-29 07:19:27 -04:00
2013-12-03 13:21:50 -05:00
if ( options . keypair ) {
2013-10-21 07:10:42 -04:00
// import existing key pair into crypto module
2014-03-05 14:14:23 -05:00
handleExistingKeypair ( options . keypair ) ;
2013-10-21 07:10:42 -04:00
return ;
}
2013-08-16 14:50:47 -04:00
2013-10-21 07:10:42 -04:00
// no keypair for is stored for the user... generate a new one
2014-06-13 06:33:30 -04:00
self . _pgp . generateKeys ( {
2013-10-21 07:10:42 -04:00
emailAddress : self . _account . emailAddress ,
keySize : self . _account . asymKeySize ,
2013-12-03 13:21:50 -05:00
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 ;
}
2013-12-03 13:21:50 -05:00
handleGenerated ( generatedKeypair ) ;
} ) ;
2014-03-05 14:14:23 -05:00
function handleExistingKeypair ( keypair ) {
2014-06-18 04:02:33 -04:00
var privKeyParams , pubKeyParams ;
try {
2014-06-13 06:33:30 -04:00
privKeyParams = self . _pgp . getKeyParams ( keypair . privateKey . encryptedKey ) ;
pubKeyParams = self . _pgp . getKeyParams ( keypair . publicKey . publicKey ) ;
2014-06-18 04:02:33 -04:00
} catch ( e ) {
callback ( new Error ( 'Error reading key params!' ) ) ;
return ;
}
2014-03-05 14:14:23 -05:00
// check if key IDs match
2014-06-18 04:02:33 -04:00
if ( ! keypair . privateKey . _id || keypair . privateKey . _id !== keypair . publicKey . _id || keypair . privateKey . _id !== privKeyParams . _id || keypair . publicKey . _id !== pubKeyParams . _id ) {
callback ( new Error ( 'Key IDs dont match!' ) ) ;
2014-03-05 14:14:23 -05:00
return ;
}
2014-06-18 04:02:33 -04:00
// check that key userIds contain email address of user account
var matchingPrivUserId = _ . findWhere ( privKeyParams . userIds , {
emailAddress : self . _account . emailAddress
} ) ;
var matchingPubUserId = _ . findWhere ( pubKeyParams . userIds , {
emailAddress : self . _account . emailAddress
} ) ;
if ( ! matchingPrivUserId || ! matchingPubUserId || keypair . privateKey . userId !== self . _account . emailAddress || keypair . publicKey . userId !== self . _account . emailAddress ) {
callback ( new Error ( 'User IDs dont match!' ) ) ;
2014-03-05 14:14:23 -05:00
return ;
}
// import existing key pair into crypto module
2014-06-13 06:33:30 -04:00
self . _pgp . importKeys ( {
2014-03-05 14:14:23 -05:00
passphrase : options . passphrase ,
privateKeyArmored : keypair . privateKey . encryptedKey ,
publicKeyArmored : keypair . publicKey . publicKey
} , function ( err ) {
if ( err ) {
callback ( err ) ;
return ;
}
// set decrypted privateKey to pgpMailer
2014-06-13 06:33:30 -04:00
self . _pgpbuilder . _privateKey = self . _pgp . _privateKey ;
2014-03-05 14:14:23 -05:00
callback ( ) ;
} ) ;
}
2013-12-03 13:21:50 -05:00
function handleGenerated ( generatedKeypair ) {
2013-10-21 07:10:42 -04:00
// import the new key pair into crypto module
2014-06-13 06:33:30 -04:00
self . _pgp . importKeys ( {
2013-12-03 13:21:50 -05:00
passphrase : options . passphrase ,
2013-10-21 07:10:42 -04:00
privateKeyArmored : generatedKeypair . privateKeyArmored ,
publicKeyArmored : generatedKeypair . publicKeyArmored
} , function ( err ) {
2013-08-16 14:50:47 -04:00
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-06-13 06:33:30 -04:00
self . _pgpbuilder . _privateKey = self . _pgp . _privateKey ;
2014-02-24 04:14:07 -05:00
callback ( ) ;
} ) ;
2013-08-16 14:50:47 -04:00
} ) ;
2013-10-10 13:15:16 -04:00
}
2013-09-20 12:44:14 -04:00
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . openFolder = function ( options , callback ) {
2014-06-02 11:54:29 -04:00
var self = this ,
err ;
2014-05-23 08:23:50 -04:00
if ( ! self . _account . online ) {
2014-06-02 11:54:29 -04:00
err = new Error ( 'Client is currently offline!' ) ;
err . code = 42 ;
callback ( err ) ;
2014-05-23 08:23:50 -04:00
return ;
}
if ( options . folder . path === config . outboxMailboxPath ) {
return ;
}
this . _imapClient . selectMailbox ( {
path : options . folder . path
} , callback ) ;
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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 ]
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . refreshFolder = function ( options , callback ) {
var self = this ,
folder = options . folder ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-05-23 08:23:50 -04:00
folder . messages = folder . messages || [ ] ;
self . _localListMessages ( {
folder : folder
} , function ( err , storedMessages ) {
if ( err ) {
done ( err ) ;
return ;
}
2014-07-31 03:57:57 -04:00
var storedUids = _ . pluck ( storedMessages , MSG _ATTR _UID ) ,
memoryUids = _ . pluck ( folder . messages , MSG _ATTR _UID ) ,
2014-06-03 06:15:41 -04:00
newUids = _ . difference ( storedUids , memoryUids ) , // uids of messages that are not yet in memory
removedUids = _ . difference ( memoryUids , storedUids ) ; // uids of messages that are no longer stored on the disk
2014-05-23 08:23:50 -04:00
// which messages are new on the disk that are not yet in memory?
_ . filter ( storedMessages , function ( msg ) {
return _ . contains ( newUids , msg . uid ) ;
} ) . forEach ( function ( newMessage ) {
// remove the body parts to not load unnecessary data to memory
// however, don't do that for the outbox. load the full message there.
if ( folder . path !== config . outboxMailboxPath ) {
delete newMessage . bodyParts ;
}
folder . messages . push ( newMessage ) ;
} ) ;
// which messages are no longer on disk, i.e. have been removed/sent/...
_ . filter ( folder . messages , function ( msg ) {
return _ . contains ( removedUids , msg . uid ) ;
} ) . forEach ( function ( removedMessage ) {
// remove the message
var index = folder . messages . indexOf ( removedMessage ) ;
folder . messages . splice ( index , 1 ) ;
} ) ;
done ( ) ;
} ) ;
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-05-23 08:23:50 -04:00
updateUnreadCount ( folder ) ; // update the unread count
callback ( err ) ;
}
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . fetchMessages = function ( options , callback ) {
var self = this ,
folder = options . folder ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-05-23 08:23:50 -04:00
if ( ! self . _account . online ) {
done ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
// list the messages starting from the lowest new uid to the highest new uid
self . _imapListMessages ( options , function ( err , messages ) {
if ( err ) {
done ( err ) ;
return ;
}
// if there are verification messages in the synced messages, handle it
var verificationMessages = _ . filter ( messages , function ( message ) {
return message . subject === str . verificationSubject ;
} ) ;
// if there are verification messages, continue after we've tried to verify
if ( verificationMessages . length > 0 ) {
var after = _ . after ( verificationMessages . length , storeHeaders ) ;
verificationMessages . forEach ( function ( verificationMessage ) {
handleVerification ( verificationMessage , function ( err , isValid ) {
// if it was NOT a valid verification mail, do nothing
// if an error occurred and the mail was a valid verification mail,
// keep the mail in the list so the user can see it and verify manually
if ( ! isValid || err ) {
after ( ) ;
return ;
}
// if verification worked, we remove the mail from the list.
messages . splice ( messages . indexOf ( verificationMessage ) , 1 ) ;
after ( ) ;
} ) ;
} ) ;
return ;
}
// no verification messages, just proceed as usual
storeHeaders ( ) ;
function storeHeaders ( ) {
if ( _ . isEmpty ( messages ) ) {
// nothing to do, we're done here
done ( ) ;
return ;
}
// persist the encrypted message to the local storage
self . _localStoreMessages ( {
folder : folder ,
emails : messages
} , function ( err ) {
if ( err ) {
done ( err ) ;
return ;
}
// this enables us to already show the attachment clip in the message list ui
messages . forEach ( function ( message ) {
message . attachments = message . bodyParts . filter ( function ( bodyPart ) {
2014-07-31 03:57:57 -04:00
return bodyPart . type === MSG _PART _TYPE _ATTACHMENT ;
2014-05-23 08:23:50 -04:00
} ) ;
} ) ;
[ ] . unshift . apply ( folder . messages , messages ) ; // add the new messages to the folder
updateUnreadCount ( folder ) ; // update the unread count
2014-09-16 13:01:49 -04:00
// notify about new messages only for the inbox
if ( folder . type === FOLDER _TYPE _INBOX ) {
self . onIncomingMessage ( messages ) ;
}
2014-05-23 08:23:50 -04:00
done ( ) ;
} ) ;
}
} ) ;
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-05-23 08:23:50 -04:00
callback ( err ) ;
}
2014-06-03 06:15:41 -04:00
// Handles verification of public keys, deletion of messages with verified keys
2014-05-23 08:23:50 -04:00
function handleVerification ( message , localCallback ) {
self . _getBodyParts ( {
folder : folder ,
uid : message . uid ,
bodyParts : message . bodyParts
} , function ( error , parsedBodyParts ) {
// we could not stream the text to determine if the verification was valid or not
// so handle it as if it were valid
if ( error ) {
localCallback ( error , true ) ;
return ;
}
2014-07-31 03:57:57 -04:00
var body = _ . pluck ( filterBodyParts ( parsedBodyParts , MSG _PART _TYPE _TEXT ) , MSG _PART _ATTR _CONTENT ) . join ( '\n' ) ,
2014-05-23 08:23:50 -04:00
verificationUrlPrefix = config . cloudUrl + config . verificationUrl ,
uuid = body . split ( verificationUrlPrefix ) . pop ( ) . substr ( 0 , config . verificationUuidLength ) ,
uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/ ;
// there's no valid uuid in the message, so forget about it
if ( ! uuidRegex . test ( uuid ) ) {
localCallback ( null , false ) ;
return ;
}
// there's a valid uuid in the message, so try to verify it
self . _keychain . verifyPublicKey ( uuid , function ( err ) {
if ( err ) {
localCallback ( {
errMsg : 'Verifying your public key failed: ' + err . errMsg
} , true ) ;
return ;
}
// public key has been verified, delete the message
self . _imapDeleteMessage ( {
folder : folder ,
uid : message . uid
} , function ( ) {
// if we could successfully not delete the message or not doesn't matter.
// just don't show it in whiteout and keep quiet about it
localCallback ( null , true ) ;
} ) ;
} ) ;
} ) ;
}
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . deleteMessage = function ( options , callback ) {
var self = this ,
folder = options . folder ,
message = options . message ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-05-23 08:23:50 -04:00
folder . messages . splice ( folder . messages . indexOf ( message ) , 1 ) ;
2014-06-03 06:15:41 -04:00
// delete only locally
2014-05-23 08:23:50 -04:00
if ( options . localOnly || options . folder . path === config . outboxMailboxPath ) {
deleteLocal ( ) ;
return ;
}
deleteImap ( ) ;
function deleteImap ( ) {
if ( ! self . _account . online ) {
2014-06-03 06:15:41 -04:00
// no action if we're not online
2014-05-23 08:23:50 -04:00
done ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
2014-06-03 06:15:41 -04:00
// delete from IMAP
2014-05-23 08:23:50 -04:00
self . _imapDeleteMessage ( {
folder : folder ,
uid : message . uid
} , function ( err ) {
if ( err ) {
done ( err ) ;
return ;
}
deleteLocal ( ) ;
} ) ;
}
function deleteLocal ( ) {
2014-06-03 06:15:41 -04:00
// delete from indexed db
2014-05-23 08:23:50 -04:00
self . _localDeleteMessage ( {
folder : folder ,
uid : message . uid
} , done ) ;
}
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-05-23 08:23:50 -04:00
if ( err ) {
folder . messages . unshift ( message ) ; // re-add the message to the folder in case of an error
}
updateUnreadCount ( folder ) ; // update the unread count, if necessary
callback ( err ) ;
}
2014-02-24 04:14:07 -05:00
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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 .
2014-06-04 12:33:07 -04:00
*
2014-06-03 06:15:41 -04:00
* @ param { [ type ] } options [ description ]
* @ param { Function } callback [ description ]
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . setFlags = function ( options , callback ) {
var self = this ,
folder = options . folder ,
message = options . message ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ; // start the spinner
2014-05-23 08:23:50 -04:00
2014-06-03 06:15:41 -04:00
// no-op if the message if not present anymore (for whatever reason)
2014-05-23 08:23:50 -04:00
if ( folder . messages . indexOf ( message ) < 0 ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-05-23 08:23:50 -04:00
return ;
}
2014-06-03 06:15:41 -04:00
// don't do a roundtrip to IMAP,
2014-06-04 12:33:07 -04:00
// especially if you want to mark outbox messages
2014-05-23 08:23:50 -04:00
if ( options . localOnly || options . folder . path === config . outboxMailboxPath ) {
markStorage ( ) ;
return ;
}
if ( ! self . _account . online ) {
2014-06-03 06:15:41 -04:00
// no action if we're not online
2014-05-23 08:23:50 -04:00
done ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
markImap ( ) ;
function markImap ( ) {
2014-06-03 06:15:41 -04:00
// mark a message unread/answered on IMAP
2014-05-23 08:23:50 -04:00
self . _imapMark ( {
folder : folder ,
uid : options . message . uid ,
unread : options . message . unread ,
answered : options . message . answered
} , function ( err ) {
if ( err ) {
done ( err ) ;
return ;
}
markStorage ( ) ;
} ) ;
}
function markStorage ( ) {
2014-06-03 06:15:41 -04:00
// 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
2014-05-23 08:23:50 -04:00
self . _localListMessages ( {
folder : folder ,
uid : options . message . uid ,
} , function ( err , storedMessages ) {
if ( err ) {
done ( err ) ;
return ;
}
2014-06-03 06:15:41 -04:00
// set the flags
2014-05-23 08:23:50 -04:00
var storedMessage = storedMessages [ 0 ] ;
storedMessage . unread = options . message . unread ;
storedMessage . answered = options . message . answered ;
2014-06-03 06:15:41 -04:00
storedMessage . modseq = options . message . modseq || storedMessage . modseq ;
2014-05-23 08:23:50 -04:00
2014-06-03 06:15:41 -04:00
// store
2014-05-23 08:23:50 -04:00
self . _localStoreMessages ( {
folder : folder ,
emails : [ storedMessage ]
} , done ) ;
} ) ;
}
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-05-23 08:23:50 -04:00
updateUnreadCount ( folder ) ; // update the unread count
callback ( err ) ;
}
2014-02-14 11:29:16 -05:00
} ;
2013-09-26 07:26:57 -04:00
2014-02-14 11:29:16 -05: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 ) {
2014-02-14 11:29:16 -05:00
var self = this ,
message = options . message ,
folder = options . folder ;
2013-10-04 09:47:30 -04:00
2014-05-06 11:23:08 -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' ) {
2014-02-14 11:29:16 -05:00
return ;
2013-12-03 13:21:50 -05:00
}
2013-09-26 07:26:57 -04:00
2014-02-17 08:31:14 -05:00
message . loadingBody = true ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-05-06 11:23:08 -04:00
/ *
* 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 .
* /
2013-09-28 13:04:15 -04:00
2014-05-06 11:23:08 -04:00
retrieveContent ( ) ;
function retrieveContent ( ) {
// load the local message from memory
2014-05-23 08:23:50 -04:00
self . _localListMessages ( {
2014-02-14 11:29:16 -05:00
folder : folder ,
uid : message . uid
} , function ( err , localMessages ) {
2014-05-06 11:23:08 -04:00
if ( err || localMessages . length === 0 ) {
done ( err ) ;
2013-09-26 07:26:57 -04:00
return ;
}
2014-05-06 11:23:08 -04:00
var localMessage = localMessages [ 0 ] ;
2014-02-14 11:29:16 -05:00
2014-05-12 16:07:25 -04:00
// treat attachment and non-attachment body parts separately:
2014-06-25 08:13:46 -04:00
// we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html)
2014-05-12 16:07:25 -04:00
// but we spare the effort and fetch attachment content later upon explicit user request.
var contentParts = localMessage . bodyParts . filter ( function ( bodyPart ) {
2014-07-31 03:57:57 -04:00
return bodyPart . type !== MSG _PART _TYPE _ATTACHMENT || ( bodyPart . type === MSG _PART _TYPE _ATTACHMENT && bodyPart . id ) ;
2014-05-12 16:07:25 -04:00
} ) ;
var attachmentParts = localMessage . bodyParts . filter ( function ( bodyPart ) {
2014-07-31 03:57:57 -04:00
return bodyPart . type === MSG _PART _TYPE _ATTACHMENT && ! bodyPart . id ;
2014-05-12 16:07:25 -04:00
} ) ;
2014-05-06 11:23:08 -04:00
// 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 ) {
2014-05-06 11:23:08 -04:00
needsFetch = ( typeof part . content === 'undefined' ) ;
} ) ;
2014-02-14 11:29:16 -05:00
2014-05-06 11:23:08 -04:00
if ( ! needsFetch ) {
// if we have all the content we need,
// we can extract the content
message . bodyParts = localMessage . bodyParts ;
extractContent ( ) ;
2014-02-14 11:29:16 -05:00
return ;
}
2013-09-26 07:26:57 -04:00
2014-05-06 11:23:08 -04:00
// get the raw content from the imap server
2014-05-23 08:23:50 -04:00
self . _getBodyParts ( {
2014-02-14 11:29:16 -05:00
folder : folder ,
2014-05-06 11:23:08 -04:00
uid : localMessage . uid ,
2014-05-12 16:07:25 -04:00
bodyParts : contentParts
2014-05-06 11:23:08 -04:00
} , 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 ) ;
2014-02-24 11:37:23 -05:00
2014-05-06 11:23:08 -04:00
// persist it to disk
2014-05-23 08:23:50 -04:00
self . _localStoreMessages ( {
2014-02-24 11:37:23 -05:00
folder : folder ,
2014-05-06 11:23:08 -04:00
emails : [ localMessage ]
2014-02-24 11:37:23 -05:00
} , function ( error ) {
if ( error ) {
2014-05-06 11:23:08 -04:00
done ( error ) ;
2014-02-24 11:37:23 -05:00
return ;
}
2014-05-06 11:23:08 -04:00
// extract the content
extractContent ( ) ;
2014-02-24 11:37:23 -05:00
} ) ;
2013-09-26 07:26:57 -04:00
} ) ;
} ) ;
2014-02-14 11:29:16 -05:00
}
2013-08-21 07:43:19 -04:00
2014-05-06 11:23:08 -04:00
function extractContent ( ) {
2014-02-14 11:29:16 -05:00
if ( message . encrypted ) {
2014-05-06 11:23:08 -04:00
// show the encrypted message
2014-07-31 03:57:57 -04:00
message . body = filterBodyParts ( message . bodyParts , MSG _PART _TYPE _ENCRYPTED ) [ 0 ] . content ;
2014-07-04 11:58:25 -04:00
return done ( ) ;
2013-08-29 10:01:40 -04:00
}
2014-02-14 11:29:16 -05:00
2014-05-06 11:23:08 -04:00
var root = message . bodyParts ;
2014-02-14 11:29:16 -05:00
2014-05-06 11:23:08 -04:00
if ( message . signed ) {
2014-07-04 11:58:25 -04:00
// PGP/MIME signed
2014-07-31 03:57:57 -04:00
var signedRoot = filterBodyParts ( message . bodyParts , MSG _PART _TYPE _SIGNED ) [ 0 ] ; // in case of a signed message, you only want to show the signed content and ignore the rest
2014-07-04 11:58:25 -04:00
message . signedMessage = signedRoot . signedMessage ;
message . signature = signedRoot . signature ;
root = signedRoot . content ;
2014-05-06 11:23:08 -04:00
}
2014-07-31 03:57:57 -04:00
var body = _ . pluck ( filterBodyParts ( root , MSG _PART _TYPE _TEXT ) , MSG _PART _ATTR _CONTENT ) . join ( '\n' ) ;
2014-05-13 07:13:36 -04:00
/ *
2014-07-04 11:58:25 -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 .
* "-----BEGIN/END (...)-----" must be at the start / 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 ,
2014-05-13 07:13:36 -04:00
* /
2014-07-04 11:58:25 -04:00
var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im . exec ( body ) ;
2014-06-27 10:19:30 -04:00
if ( pgpInlineMatch ) {
2014-07-04 11:58:25 -04:00
message . body = pgpInlineMatch [ 0 ] ; // show the plain text content
message . encrypted = true ; // signal the ui that we're handling encrypted content
2014-05-13 07:13:36 -04:00
2014-07-04 11:58:25 -04:00
// replace the bodyParts info with an artificial bodyPart of type "encrypted"
2014-05-13 07:13:36 -04:00
message . bodyParts = [ {
2014-07-31 03:57:57 -04:00
type : MSG _PART _TYPE _ENCRYPTED ,
2014-06-27 10:19:30 -04:00
content : pgpInlineMatch [ 0 ] ,
2014-07-04 11:58:25 -04:00
_isPgpInline : true // used internally to avoid trying to parse non-MIME text with the mailreader
2014-05-13 07:13:36 -04:00
} ] ;
2014-07-04 11:58:25 -04:00
return done ( ) ;
2014-05-13 07:13:36 -04:00
}
2014-06-27 10:19:30 -04:00
/ *
2014-07-04 11:58:25 -04:00
* any content before / after the PGP block will be discarded ,
* "-----BEGIN/END (...)-----" must be at the start / end of a line ,
2014-09-12 11:02:03 -04:00
* after the hash ( and possibly other ) arbitrary headers , the signed payload begins ,
2014-07-04 11:58:25 -04:00
* the text is followed by a final \ n and then the pgp signature begins
* untrusted attachments and html is ignored
2014-06-27 10:19:30 -04:00
* /
2014-09-12 11:02:03 -04:00
var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im . exec ( body ) ;
2014-06-27 10:19:30 -04:00
if ( clearSignedMatch ) {
2014-07-04 11:58:25 -04:00
// PGP/INLINE signed
2014-06-27 10:19:30 -04:00
message . signed = true ;
2014-07-04 11:58:25 -04:00
message . clearSignedMessage = clearSignedMatch [ 0 ] ;
body = clearSignedMatch [ 1 ] ;
}
2014-06-27 10:19:30 -04:00
2014-07-04 11:58:25 -04:00
if ( ! message . signed ) {
// message is not signed, so we're done here
return setBody ( ) ;
2014-06-27 10:19:30 -04:00
}
2014-07-04 11:58:25 -04:00
// check the signatures for signed messages
self . _checkSignatures ( message , function ( err , signaturesValid ) {
if ( err ) {
return done ( err ) ;
}
2014-02-14 11:29:16 -05:00
2014-07-04 11:58:25 -04:00
message . signaturesValid = signaturesValid ;
setBody ( ) ;
} ) ;
function setBody ( ) {
message . body = body ;
if ( ! message . clearSignedMessage ) {
2014-07-31 03:57:57 -04:00
message . attachments = filterBodyParts ( root , MSG _PART _TYPE _ATTACHMENT ) ;
message . html = _ . pluck ( filterBodyParts ( root , MSG _PART _TYPE _HTML ) , MSG _PART _ATTR _CONTENT ) . join ( '\n' ) ;
2014-07-04 11:58:25 -04:00
inlineExternalImages ( message ) ;
}
done ( ) ;
}
2013-10-16 12:56:18 -04:00
}
2014-02-14 11:29:16 -05:00
2014-06-25 08:13:46 -04:00
2014-05-06 11:23:08 -04:00
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ;
2014-05-06 11:23:08 -04:00
message . loadingBody = false ;
callback ( err , err ? undefined : message ) ;
}
2014-02-14 11:29:16 -05:00
} ;
2014-07-04 11:58:25 -04:00
EmailDAO . prototype . _checkSignatures = function ( message , callback ) {
var self = this ;
self . _keychain . getReceiverPublicKey ( message . from [ 0 ] . address , function ( err , senderPublicKey ) {
if ( err ) {
return callback ( err ) ;
}
// get the receiver's public key to check the message signature
var senderKey = senderPublicKey ? senderPublicKey . publicKey : undefined ;
if ( message . clearSignedMessage ) {
self . _pgp . verifyClearSignedMessage ( message . clearSignedMessage , senderKey , callback ) ;
} else if ( message . signedMessage && message . signature ) {
self . _pgp . verifySignedMessage ( message . signedMessage , message . signature , senderKey , callback ) ;
} else {
callback ( null , undefined ) ;
}
} ) ;
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-12 16:07:25 -04:00
EmailDAO . prototype . getAttachment = function ( options , callback ) {
2014-09-18 12:04:20 -04:00
var self = this ,
attachment = options . attachment ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-09-18 12:04:20 -04:00
attachment . busy = true ;
2014-07-11 09:03:03 -04:00
self . _getBodyParts ( {
2014-05-12 16:07:25 -04:00
folder : options . folder ,
uid : options . uid ,
2014-09-18 12:04:20 -04:00
bodyParts : [ attachment ]
2014-05-12 16:07:25 -04:00
} , function ( err , parsedBodyParts ) {
2014-09-18 12:04:20 -04:00
attachment . busy = false ;
2014-05-12 16:07:25 -04:00
if ( err ) {
callback ( err ) ;
return ;
}
2014-07-11 09:03:03 -04:00
self . done ( ) ;
2014-06-03 06:15:41 -04:00
// add the content to the original object
2014-09-18 12:04:20 -04:00
attachment . content = parsedBodyParts [ 0 ] . content ;
callback ( err , err ? undefined : attachment ) ;
2014-05-12 16:07:25 -04:00
} ) ;
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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 )
* /
2014-02-24 04:14:07 -05:00
EmailDAO . prototype . decryptBody = function ( options , callback ) {
2014-02-14 11:29:16 -05:00
var self = this ,
message = options . message ;
2014-05-06 11:23:08 -04:00
// the message is decrypting has no body, is not encrypted or has already been decrypted
2014-05-23 08:23:50 -04:00
if ( ! message . bodyParts || message . decryptingBody || ! message . body || ! message . encrypted || message . decrypted ) {
2014-06-04 12:33:07 -04:00
callback ( null , message ) ;
2014-02-14 11:29:16 -05:00
return ;
}
2014-02-17 08:31:14 -05:00
message . decryptingBody = true ;
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-02-14 11:29:16 -05:00
// get the sender's public key for signature checking
self . _keychain . getReceiverPublicKey ( message . from [ 0 ] . address , function ( err , senderPublicKey ) {
if ( err ) {
2014-07-11 09:03:03 -04:00
return done ( err ) ;
2014-02-14 11:29:16 -05:00
}
// get the receiver's public key to check the message signature
2014-07-31 03:57:57 -04:00
var encryptedNode = filterBodyParts ( message . bodyParts , MSG _PART _TYPE _ENCRYPTED ) [ 0 ] ;
2014-07-01 17:28:44 -04:00
var senderKey = senderPublicKey ? senderPublicKey . publicKey : undefined ;
self . _pgp . decrypt ( encryptedNode . content , senderKey , function ( err , decrypted , signaturesValid ) {
2014-05-06 11:23:08 -04:00
if ( err || ! decrypted ) {
2014-06-27 10:19:30 -04:00
return showError ( err . message || 'An error occurred during the decryption.' ) ;
}
2014-07-01 17:28:44 -04:00
// if the decryption worked and signatures are present, everything's fine.
// no error is thrown if signatures are not present
message . signed = typeof signaturesValid !== 'undefined' ;
message . signaturesValid = signaturesValid ;
2014-05-13 07:13:36 -04:00
// 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 ;
2014-07-11 09:03:03 -04:00
return done ( ) ;
2014-02-14 11:29:16 -05:00
}
2014-05-06 11:23:08 -04:00
// 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
2014-05-06 11:23:08 -04:00
self . _mailreader . parse ( {
bodyParts : [ encryptedNode ]
2014-07-04 11:58:25 -04:00
} , function ( err , root ) {
2014-05-12 07:44:02 -04:00
if ( err ) {
2014-07-11 09:03:03 -04:00
return showError ( err . errMsg || err . message ) ;
2014-02-14 11:29:16 -05:00
}
2014-07-04 11:58:25 -04:00
if ( ! message . signed ) {
// message had no signature in the ciphertext, so there's a little extra effort to be done here
// is there a signed MIME node?
2014-07-31 03:57:57 -04:00
var signedRoot = filterBodyParts ( root , MSG _PART _TYPE _SIGNED ) [ 0 ] ;
2014-07-04 11:58:25 -04:00
if ( ! signedRoot ) {
// no signed MIME node, obviously an unsigned PGP/MIME message
return setBody ( ) ;
}
2014-02-14 11:29:16 -05:00
2014-07-04 11:58:25 -04:00
// if there is something signed in here, we're only interested in the signed content
message . signedMessage = signedRoot . signedMessage ;
message . signature = signedRoot . signature ;
root = signedRoot . content ;
2014-05-06 11:23:08 -04:00
2014-07-04 11:58:25 -04:00
// check the signatures for encrypted messages
self . _checkSignatures ( message , function ( err , signaturesValid ) {
if ( err ) {
return done ( err ) ;
}
message . signed = typeof signaturesValid !== 'undefined' ;
message . signaturesValid = signaturesValid ;
setBody ( ) ;
} ) ;
return ;
}
// message had a signature in the ciphertext, so we're done here
setBody ( ) ;
function setBody ( ) {
// we have successfully interpreted the descrypted message,
// so let's update the views on the message parts
2014-07-31 03:57:57 -04:00
message . body = _ . pluck ( filterBodyParts ( root , MSG _PART _TYPE _TEXT ) , MSG _PART _ATTR _CONTENT ) . join ( '\n' ) ;
message . html = _ . pluck ( filterBodyParts ( root , MSG _PART _TYPE _HTML ) , MSG _PART _ATTR _CONTENT ) . join ( '\n' ) ;
message . attachments = _ . reject ( filterBodyParts ( root , MSG _PART _TYPE _ATTACHMENT ) , function ( attmt ) {
2014-07-04 11:58:25 -04:00
// remove the pgp-signature from the attachments
return attmt . mimeType === "application/pgp-signature" ;
} ) ;
inlineExternalImages ( message ) ;
message . decrypted = true ;
// we're done here!
done ( ) ;
}
2014-02-14 11:29:16 -05:00
} ) ;
} ) ;
} ) ;
2013-08-20 07:30:35 -04:00
2014-05-12 07:44:02 -04:00
function showError ( msg ) {
message . body = msg ;
2014-07-04 11:58:25 -04:00
message . decrypted = true ; // display error msg in body
2014-05-12 07:44:02 -04:00
done ( ) ;
}
2014-05-06 11:23:08 -04:00
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ;
2014-05-06 11:23:08 -04:00
message . decryptingBody = false ;
callback ( err , err ? undefined : message ) ;
2014-01-16 09:37:08 -05:00
}
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2013-12-03 13:21:50 -05:00
EmailDAO . prototype . sendEncrypted = function ( options , callback ) {
2014-02-24 04:14:07 -05:00
var self = this ;
2013-10-04 10:29:32 -04:00
2014-05-23 08:23:50 -04:00
if ( ! self . _account . online ) {
2013-12-09 13:21:52 -05:00
callback ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
2014-07-11 09:03:03 -04:00
self . busy ( ) ;
2014-02-24 04:14:07 -05:00
// mime encode, sign, encrypt and send email via smtp
self . _pgpMailer . send ( {
encrypt : true ,
2014-06-17 06:41:22 -04:00
smtpclient : options . smtpclient , // filled solely in the integration test, undefined in normal usage
2014-02-24 04:14:07 -05:00
mail : options . email ,
publicKeysArmored : options . email . publicKeysArmored
2014-07-01 13:49:19 -04:00
} , function ( err , rfcText ) {
if ( err ) {
return callback ( err ) ;
}
2014-07-23 07:56:10 -04:00
// upload the sent message to the sent folder if necessary
2014-07-01 13:49:19 -04:00
var sentFolder = _ . findWhere ( self . _account . folders , {
2014-07-31 03:57:57 -04:00
type : FOLDER _TYPE _SENT
2014-07-01 13:49:19 -04:00
} ) ;
if ( self . ignoreUploadOnSent || ! sentFolder || ! rfcText ) {
2014-07-23 11:20:57 -04:00
self . done ( ) ;
2014-07-01 13:49:19 -04:00
return callback ( ) ;
}
self . _imapClient . uploadMessage ( {
path : sentFolder . path ,
message : rfcText
} , function ( err ) {
self . done ( ) ;
callback ( err ) ;
} ) ;
2014-07-11 09:03:03 -04:00
} ) ;
2013-10-04 10:29:32 -04:00
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2013-12-03 13:21:50 -05:00
EmailDAO . prototype . sendPlaintext = function ( options , callback ) {
2014-07-11 09:03:03 -04:00
var self = this ;
if ( ! self . _account . online ) {
2013-12-09 13:21:52 -05:00
callback ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
2014-07-11 09:22:34 -04:00
self . busy ( ) ;
2014-07-29 08:13:00 -04:00
// add suffix to plaintext mail
options . email . body += str . signature + config . cloudUrl + '/' + self . _account . emailAddress ;
2014-02-03 16:07:39 -05:00
// mime encode, sign and send email via smtp
2014-07-11 09:03:03 -04:00
self . _pgpMailer . send ( {
2014-06-17 06:41:22 -04:00
smtpclient : options . smtpclient , // filled solely in the integration test, undefined in normal usage
2014-02-03 16:07:39 -05:00
mail : options . email
2014-07-01 13:49:19 -04:00
} , function ( err , rfcText ) {
if ( err ) {
return callback ( err ) ;
}
2014-07-23 07:56:10 -04:00
// upload the sent message to the sent folder if necessary
2014-07-01 13:49:19 -04:00
var sentFolder = _ . findWhere ( self . _account . folders , {
2014-07-31 03:57:57 -04:00
type : FOLDER _TYPE _SENT
2014-07-01 13:49:19 -04:00
} ) ;
if ( self . ignoreUploadOnSent || ! sentFolder || ! rfcText ) {
2014-07-23 11:20:57 -04:00
self . done ( ) ;
2014-07-01 13:49:19 -04:00
return callback ( ) ;
}
self . _imapClient . uploadMessage ( {
path : sentFolder . path ,
message : rfcText
} , function ( err ) {
self . done ( ) ;
callback ( err ) ;
} ) ;
2014-07-11 09:03:03 -04:00
} ) ;
2013-12-03 13:21:50 -05:00
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-02-24 04:14:07 -05:00
EmailDAO . prototype . encrypt = function ( options , callback ) {
2014-07-11 09:22:34 -04:00
var self = this ;
self . busy ( ) ;
self . _pgpbuilder . encrypt ( options , function ( err ) {
self . done ( ) ;
callback ( err ) ;
} ) ;
2014-02-24 04:14:07 -05:00
} ;
2014-05-23 08:23:50 -04:00
//
//
// Event Handlers
//
//
2014-06-03 06:15:41 -04:00
/ * *
* 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 ]
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . onConnect = function ( options , callback ) {
var self = this ;
self . _account . loggingIn = true ;
self . _imapClient = options . imapClient ;
self . _pgpMailer = options . pgpMailer ;
2014-06-02 11:54:29 -04:00
self . _imapClient . login ( function ( err ) {
2014-05-23 08:23:50 -04:00
self . _account . loggingIn = false ;
if ( err ) {
callback ( err ) ;
return ;
}
// init folders
2014-06-23 07:03:56 -04:00
self . _initFoldersFromImap ( function ( err ) {
2014-05-23 08:23:50 -04:00
if ( err ) {
callback ( err ) ;
return ;
}
// attach sync update handler
self . _imapClient . onSyncUpdate = self . _onSyncUpdate . bind ( self ) ;
2014-06-03 06:15:41 -04:00
// 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
2014-05-23 08:23:50 -04:00
var mailboxCache = { } ;
self . _account . folders . forEach ( function ( folder ) {
if ( folder . messages . length === 0 ) {
return ;
}
var uids , highestModseq , lastUid ;
2014-07-31 03:57:57 -04:00
uids = _ . pluck ( folder . messages , MSG _ATTR _UID ) . sort ( function ( a , b ) {
2014-05-23 08:23:50 -04:00
return a - b ;
} ) ;
lastUid = uids [ uids . length - 1 ] ;
highestModseq = _ . pluck ( folder . messages , 'modseq' ) . sort ( function ( a , b ) {
return a - b ;
} ) . pop ( ) ;
mailboxCache [ folder . path ] = {
exists : lastUid ,
uidNext : lastUid + 1 ,
uidlist : uids ,
highestModseq : highestModseq
} ;
} ) ;
self . _imapClient . mailboxCache = mailboxCache ;
2014-06-23 04:59:17 -04:00
// set status to online after setting cache to prevent race condition
self . _account . online = true ;
2014-06-03 06:15:41 -04:00
// set up the imap client to listen for changes in the inbox
2014-05-23 08:23:50 -04:00
var inbox = _ . findWhere ( self . _account . folders , {
2014-07-31 03:57:57 -04:00
type : FOLDER _TYPE _INBOX
2014-05-23 08:23:50 -04:00
} ) ;
if ( ! inbox ) {
2014-07-01 13:49:19 -04:00
return callback ( ) ;
2014-05-23 08:23:50 -04:00
}
self . _imapClient . listenForChanges ( {
path : inbox . path
} , callback ) ;
} ) ;
} ) ;
} ;
2014-06-03 06:15:41 -04:00
/ * *
* This handler should be invoked when navigator . onLine === false . It will discard
* the imap client and pgp mailer
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . onDisconnect = function ( ) {
this . _account . online = false ;
this . _imapClient = undefined ;
this . _pgpMailer = undefined ;
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _onSyncUpdate = function ( options ) {
var self = this ;
var folder = _ . findWhere ( self . _account . folders , {
path : options . path
} ) ;
if ( ! folder ) {
// ignore updates for an unknown folder
return ;
}
2014-07-31 03:57:57 -04:00
if ( options . type === SYNC _TYPE _NEW ) {
2014-05-23 08:23:50 -04:00
// new messages available on imap, fetch from imap and store to disk and memory
self . fetchMessages ( {
folder : folder ,
firstUid : Math . min . apply ( null , options . list ) ,
lastUid : Math . max . apply ( null , options . list )
} , self . onError . bind ( self ) ) ;
2014-07-31 03:57:57 -04:00
} else if ( options . type === SYNC _TYPE _DELETED ) {
2014-05-23 08:23:50 -04:00
// messages have been deleted, remove from local storage and memory
options . list . forEach ( function ( uid ) {
var message = _ . findWhere ( folder . messages , {
uid : uid
} ) ;
if ( ! message ) {
return ;
}
self . deleteMessage ( {
folder : folder ,
message : message ,
localOnly : true
} , self . onError . bind ( self ) ) ;
} ) ;
2014-07-31 03:57:57 -04:00
} else if ( options . type === SYNC _TYPE _MSGS ) {
2014-05-23 08:23:50 -04:00
// NB! several possible reasons why this could be called.
// if a message in the array has uid value and flag array, it had a possible flag update
options . list . forEach ( function ( changedMsg ) {
if ( ! changedMsg . uid || ! changedMsg . flags ) {
return ;
}
var message = _ . findWhere ( folder . messages , {
uid : changedMsg . uid
} ) ;
if ( ! message ) {
return ;
}
2014-06-03 06:15:41 -04:00
// update unread, answered, modseq to the latest info
2014-05-23 08:23:50 -04:00
message . answered = changedMsg . flags . indexOf ( '\\Answered' ) > - 1 ;
message . unread = changedMsg . flags . indexOf ( '\\Seen' ) === - 1 ;
2014-06-03 06:15:41 -04:00
message . modseq = changedMsg . modseq ;
2014-05-23 08:23:50 -04:00
self . setFlags ( {
folder : folder ,
2014-06-03 06:15:41 -04:00
message : message ,
localOnly : true
2014-05-23 08:23:50 -04:00
} , self . onError . bind ( self ) ) ;
} ) ;
}
} ;
//
2013-12-03 13:21:50 -05:00
//
// Internal API
//
2014-05-23 08:23:50 -04:00
//
/ * *
2014-06-23 07:03:56 -04:00
* Updates the folder information from memory , and adds / removes folders in account . folders .
* The locally available messages are loaded from memory
2014-06-03 06:15:41 -04:00
*
* @ param { Function } callback Invoked when the folders are up to date
2014-05-23 08:23:50 -04:00
* /
2014-06-23 07:03:56 -04:00
EmailDAO . prototype . _initFoldersFromDisk = function ( callback ) {
2014-07-31 03:57:57 -04:00
var self = this ;
2014-05-23 08:23:50 -04:00
2014-07-11 09:03:03 -04:00
self . busy ( ) ; // start the spinner
2014-05-23 08:23:50 -04:00
2014-06-23 07:03:56 -04:00
// fetch list from local cache
2014-07-31 03:57:57 -04:00
self . _devicestorage . listItems ( FOLDER _DB _TYPE , 0 , null , function ( err , stored ) {
2014-06-23 07:03:56 -04:00
if ( err ) {
return done ( err ) ;
}
2014-05-23 08:23:50 -04:00
2014-06-23 07:03:56 -04:00
self . _account . folders = stored [ 0 ] || [ ] ;
self . _initMessagesFromDisk ( done ) ;
} ) ;
2014-05-23 08:23:50 -04:00
2014-06-23 07:03:56 -04:00
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-06-23 07:03:56 -04:00
callback ( err ) ;
}
} ;
2014-05-23 08:23:50 -04:00
2014-06-23 07:03:56 -04:00
/ * *
* Updates the folder information from imap ( if we ' re online ) . Adds / removes folders in account . folders ,
2014-06-25 08:13:46 -04:00
* if we added / removed folder in IMAP . If we have an uninitialized folder that lacks folder . messages ,
2014-06-23 07:03:56 -04:00
* all the locally available messages are loaded from memory .
*
* @ param { Function } callback Invoked when the folders are up to date
* /
EmailDAO . prototype . _initFoldersFromImap = function ( callback ) {
2014-07-31 03:57:57 -04:00
var self = this ;
2014-05-23 08:23:50 -04:00
2014-07-11 09:03:03 -04:00
self . busy ( ) ; // start the spinner
2014-05-23 08:23:50 -04:00
2014-06-23 07:03:56 -04:00
// fetch list from imap server
self . _imapClient . listWellKnownFolders ( function ( err , wellKnownFolders ) {
if ( err ) {
return done ( err ) ;
2014-05-23 08:23:50 -04:00
}
2014-07-31 03:57:57 -04:00
// initialize the folders to something meaningful if that hasn't already happened
self . _account . folders = self . _account . folders || [ ] ;
// smuggle the outbox into the well known folders, which is obv not present on imap...
wellKnownFolders [ config . outboxMailboxType ] = [ {
name : config . outboxMailboxName ,
type : config . outboxMailboxType ,
2014-07-01 13:49:19 -04:00
path : config . outboxMailboxPath
2014-07-31 03:57:57 -04:00
} ] ;
// indicates if we need to persist anything to disk
var foldersChanged = false ;
// the folders listed in the navigation pane
[ FOLDER _TYPE _INBOX , FOLDER _TYPE _SENT , config . outboxMailboxType , FOLDER _TYPE _DRAFTS , FOLDER _TYPE _TRASH ] . forEach ( function ( mbxType ) {
var localFolderWithType , imapFolderWithPath ;
// check if there is a folder of this type locally available
localFolderWithType = _ . findWhere ( self . _account . folders , {
type : mbxType
} ) ;
if ( localFolderWithType ) {
// we have a local folder available, so let's check if this folder still exists on imap
2014-06-23 07:03:56 -04:00
2014-07-31 03:57:57 -04:00
imapFolderWithPath = _ . findWhere ( wellKnownFolders [ mbxType ] , {
path : localFolderWithType . path
} ) ;
2014-06-23 07:03:56 -04:00
2014-07-31 03:57:57 -04:00
if ( imapFolderWithPath ) {
// folder present on imap, no need to update.
return ;
}
// folder not present on imap, so remove the folder and see if there are any updates for this folder type
self . _account . folders . splice ( self . _account . folders . indexOf ( localFolderWithType ) , 1 ) ;
2014-06-23 07:03:56 -04:00
foldersChanged = true ;
}
2014-05-23 08:23:50 -04:00
2014-07-31 03:57:57 -04:00
if ( ! wellKnownFolders [ mbxType ] || ! wellKnownFolders [ mbxType ] . length ) {
// no imap folders of the respective mailbox type, so nothing to do here
return ;
2014-05-23 08:23:50 -04:00
}
2014-07-31 03:57:57 -04:00
/ * *
* we have no local folder of the type , so do something intelligent ,
* i . e . take the first folder of the respective type
* /
self . _account . folders . push ( wellKnownFolders [ mbxType ] [ 0 ] ) ;
foldersChanged = true ;
2014-06-23 07:03:56 -04:00
} ) ;
2014-05-23 08:23:50 -04:00
2014-07-31 03:57:57 -04:00
// if folders have not changed, can fill them with messages directly
2014-06-23 07:03:56 -04:00
if ( ! foldersChanged ) {
return self . _initMessagesFromDisk ( done ) ;
}
2013-12-03 13:21:50 -05:00
2014-06-23 07:03:56 -04:00
// persist encrypted list in device storage
2014-07-31 03:57:57 -04:00
// note: the folders in the ui also include the messages array, so let's create a clean array here
var folders = self . _account . folders . map ( function ( folder ) {
return {
name : folder . name ,
path : folder . path ,
type : folder . type
} ;
} ) ;
self . _devicestorage . storeList ( [ folders ] , FOLDER _DB _TYPE , function ( err ) {
2014-06-23 07:03:56 -04:00
if ( err ) {
return done ( err ) ;
}
self . _initMessagesFromDisk ( done ) ;
2014-05-23 08:23:50 -04:00
} ) ;
2014-06-23 07:03:56 -04:00
} ) ;
2014-05-23 08:23:50 -04:00
function done ( err ) {
2014-07-11 09:03:03 -04:00
self . done ( ) ; // stop the spinner
2014-05-23 08:23:50 -04:00
callback ( err ) ;
}
} ;
2014-06-23 07:03:56 -04:00
/ * *
* Fill uninitialized folders with the locally available messages .
*
* @ param { Function } callback Invoked when the folders are filled with messages
* /
EmailDAO . prototype . _initMessagesFromDisk = function ( callback ) {
var self = this ;
if ( ! self . _account . folders || self . _account . folders . length === 0 ) {
return callback ( ) ;
}
var after = _ . after ( self . _account . folders . length , callback ) ;
self . _account . folders . forEach ( function ( folder ) {
if ( folder . messages ) {
// the folder is already initialized
return after ( ) ;
}
// sync messages from disk to the folder model
self . refreshFolder ( {
folder : folder
} , function ( err ) {
if ( err ) {
return callback ( err ) ;
}
after ( ) ;
} ) ;
} ) ;
} ;
2014-07-11 09:03:03 -04:00
EmailDAO . prototype . busy = function ( ) {
this . _account . busy ++ ;
} ;
EmailDAO . prototype . done = function ( ) {
if ( this . _account . busy > 0 ) {
this . _account . busy -- ;
}
} ;
2014-05-23 08:23:50 -04:00
//
2014-06-02 11:54:29 -04:00
//
2013-12-03 13:21:50 -05:00
// IMAP API
2014-06-02 11:54:29 -04:00
//
//
2013-12-03 13:21:50 -05:00
2013-10-04 10:29:32 -04:00
/ * *
2014-06-03 06:15:41 -04:00
* Mark messages as un - / r e a d o r u n - / a n s w e r e d o n I M A P
*
* @ 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 - / R e a d f l a g
* @ param { Number } options . answered Un - / A n s w e r e d f l a g
2013-10-04 10:29:32 -04:00
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _imapMark = function ( options , callback ) {
if ( ! this . _account . online ) {
callback ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
options . path = options . folder . path ;
this . _imapClient . updateFlags ( options , callback ) ;
} ;
2014-06-03 06:15:41 -04:00
/ * *
* 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 .
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _imapDeleteMessage = function ( options , callback ) {
if ( ! this . _account . online ) {
2013-12-09 13:21:52 -05:00
callback ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
2014-05-23 08:23:50 -04:00
var trash = _ . findWhere ( this . _account . folders , {
2014-07-31 03:57:57 -04:00
type : FOLDER _TYPE _TRASH
2014-05-23 08:23:50 -04:00
} ) ;
// there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message
if ( ! trash || options . folder === trash ) {
this . _imapClient . deleteMessage ( {
path : options . folder . path ,
uid : options . uid
} , callback ) ;
return ;
}
2014-06-03 06:15:41 -04:00
// move the message to the trash folder
2014-05-23 08:23:50 -04:00
this . _imapClient . moveMessage ( {
path : options . folder . path ,
destination : trash . path ,
uid : options . uid
} , callback ) ;
2013-12-03 13:21:50 -05:00
} ;
2013-10-04 10:29:32 -04:00
2013-12-03 13:21:50 -05:00
/ * *
2014-06-03 06:15:41 -04:00
* Get list messsage headers without the body
*
2014-05-23 08:23:50 -04:00
* @ param { String } options . folder The folder
* @ param { Number } options . firstUid The lower bound of the uid ( inclusive )
* @ param { Number } options . lastUid The upper bound of the uid range ( inclusive )
* @ param { Function } callback ( error , messages ) The callback when the imap client is done fetching message metadata
2013-12-03 13:21:50 -05:00
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _imapListMessages = function ( options , callback ) {
var self = this ;
2013-12-12 08:47:04 -05:00
if ( ! this . _account . online ) {
2013-12-09 13:21:52 -05:00
callback ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
2014-05-23 08:23:50 -04:00
options . path = options . folder . path ;
self . _imapClient . listMessages ( options , callback ) ;
2013-12-03 13:21:50 -05:00
} ;
2013-10-04 10:29:32 -04:00
2013-12-03 13:21:50 -05:00
/ * *
2014-05-23 08:23:50 -04:00
* Stream an email messsage ' s body
* @ param { String } options . folder The folder
* @ param { String } options . uid the message ' s uid
* @ param { Object } options . bodyParts The message , as retrieved by _imapListMessages
* @ param { Function } callback ( error , message ) The callback when the imap client is done streaming message text content
2013-12-03 13:21:50 -05:00
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _getBodyParts = function ( options , callback ) {
var self = this ;
if ( ! self . _account . online ) {
callback ( {
errMsg : 'Client is currently offline!' ,
code : 42
} ) ;
return ;
}
2013-12-03 13:21:50 -05:00
2014-05-23 08:23:50 -04:00
options . path = options . folder . path ;
self . _imapClient . getBodyParts ( options , function ( err ) {
2013-12-03 13:21:50 -05:00
if ( err ) {
callback ( err ) ;
return ;
}
2014-05-23 08:23:50 -04:00
// interpret the raw content of the email
self . _mailreader . parse ( options , callback ) ;
2013-12-03 13:21:50 -05:00
} ) ;
2014-05-23 08:23:50 -04:00
} ;
2013-12-03 13:21:50 -05:00
2014-06-02 11:54:29 -04:00
//
//
2014-05-23 08:23:50 -04:00
// Local Storage API
2014-06-02 11:54:29 -04:00
//
//
2013-12-09 13:21:52 -05:00
2013-12-03 13:21:50 -05:00
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _localListMessages = function ( options , callback ) {
var dbType = 'email_' + options . folder . path + ( options . uid ? '_' + options . uid : '' ) ;
this . _devicestorage . listItems ( dbType , 0 , null , callback ) ;
} ;
2013-12-03 13:21:50 -05:00
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _localStoreMessages = function ( options , callback ) {
var dbType = 'email_' + options . folder . path ;
this . _devicestorage . storeList ( options . emails , dbType , callback ) ;
} ;
2013-12-03 13:21:50 -05:00
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
EmailDAO . prototype . _localDeleteMessage = function ( options , callback ) {
var path = options . folder . path ,
uid = options . uid ,
id = options . id ;
if ( ! path || ! ( uid || id ) ) {
callback ( {
errMsg : 'Invalid options!'
2013-12-03 13:21:50 -05:00
} ) ;
2014-05-23 08:23:50 -04:00
return ;
2013-12-03 13:21:50 -05:00
}
2014-05-23 08:23:50 -04:00
var dbType = 'email_' + path + '_' + ( uid || id ) ;
this . _devicestorage . removeList ( dbType , callback ) ;
2013-10-04 10:29:32 -04:00
} ;
2014-05-23 08:23:50 -04:00
//
//
// Helper Functions
//
//
2014-07-11 09:03:03 -04:00
2014-06-03 06:15:41 -04:00
/ * *
* 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
* /
2014-05-23 08:23:50 -04:00
function updateUnreadCount ( folder ) {
var allMsgs = folder . messages . length ,
unreadMsgs = _ . filter ( folder . messages , function ( msg ) {
return msg . unread ;
} ) . length ;
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
2014-06-03 06:15:41 -04:00
*
* @ param { Array } bodyParts The bodyParts array
* @ param { String } type The type to look up
2014-05-23 08:23:50 -04:00
* @ param { undefined } result Leave undefined , only used for recursion
* /
function filterBodyParts ( bodyParts , type , result ) {
result = result || [ ] ;
bodyParts . forEach ( function ( part ) {
if ( part . type === type ) {
result . push ( part ) ;
} else if ( Array . isArray ( part . content ) ) {
filterBodyParts ( part . content , type , result ) ;
}
} ) ;
return result ;
}
2014-06-25 08:13:46 -04:00
/ * *
* Helper function that looks through the HTML content for < img src = "cid:..." > and
2014-06-25 10:05:14 -04:00
* inlines the images linked internally . Manipulates message . html as a side - effect .
* If no attachment matching the internal reference is found , or constructing a data
* uri fails , just remove the source .
2014-06-25 08:13:46 -04:00
*
* @ param { Object } message DTO
* /
function inlineExternalImages ( message ) {
2014-06-25 10:05:14 -04:00
message . html = message . html . replace ( /(<img[^>]+\bsrc=['"])cid:([^'">]+)(['"])/ig , function ( match , prefix , src , suffix ) {
var localSource = '' ,
payload = '' ;
2014-06-25 08:13:46 -04:00
2014-06-25 10:05:14 -04:00
var internalReference = _ . findWhere ( message . attachments , {
id : src
2014-06-25 08:13:46 -04:00
} ) ;
if ( internalReference ) {
for ( var i = 0 ; i < internalReference . content . byteLength ; i ++ ) {
payload += String . fromCharCode ( internalReference . content [ i ] ) ;
}
try {
2014-06-25 10:05:14 -04:00
localSource = 'data:application/octet-stream;base64,' + btoa ( payload ) ; // try to replace the source
} catch ( e ) { }
2014-06-25 08:13:46 -04:00
}
2014-06-25 10:05:14 -04:00
return prefix + localSource + suffix ;
} ) ;
2014-06-25 08:13:46 -04:00
}
2013-08-16 14:50:47 -04:00
return EmailDAO ;
2014-06-18 04:02:33 -04:00
} ) ;