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'),
|
2013-09-15 13:00:35 -04:00
|
|
|
str = require('js/app-config').string;
|
2013-08-27 13:17:06 -04:00
|
|
|
|
2013-08-19 15:13:32 -04:00
|
|
|
/**
|
|
|
|
* 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-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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2013-08-30 05:42:32 -04:00
|
|
|
if (!util.validateEmailAddress(i.address)) {
|
2013-08-27 12:23:27 -04:00
|
|
|
invalidRecipient = i.address;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (invalidRecipient) {
|
|
|
|
callback({
|
|
|
|
errMsg: 'Invalid recipient: ' + invalidRecipient
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2013-08-30 05:42:32 -04:00
|
|
|
if (!util.validateEmailAddress(email.from[0].address)) {
|
2013-08-27 12:23:27 -04:00
|
|
|
callback({
|
|
|
|
errMsg: 'Invalid sender: ' + email.from
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2013-08-29 13:32:34 -04:00
|
|
|
// user hasn't registered a public key yet... invite
|
2013-08-31 11:13:08 -04:00
|
|
|
self.encryptForNewUser(email, callback);
|
2013-08-27 12:23:27 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// public key found... encrypt and send
|
2013-08-29 08:28:26 -04:00
|
|
|
self.encryptForUser(email, receiverPubkey, callback);
|
2013-08-27 12:23:27 -04:00
|
|
|
});
|
2013-08-29 08:28:26 -04:00
|
|
|
};
|
2013-08-27 12:23:27 -04:00
|
|
|
|
2013-08-29 08:28:26 -04:00
|
|
|
/**
|
|
|
|
* Encrypt an email asymmetrically for an exisiting user with their public key
|
|
|
|
*/
|
|
|
|
EmailDAO.prototype.encryptForUser = function(email, receiverPubkey, callback) {
|
|
|
|
var self = this,
|
2013-08-31 11:13:08 -04:00
|
|
|
ptItems = bundleForEncryption(email),
|
|
|
|
receiverPubkeys = [receiverPubkey];
|
2013-08-27 12:23:27 -04:00
|
|
|
|
2013-08-29 08:28:26 -04:00
|
|
|
// encrypt the email
|
|
|
|
crypto.encryptListForUser(ptItems, receiverPubkeys, function(err, encryptedList) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
2013-08-28 08:12:39 -04:00
|
|
|
}
|
2013-08-27 12:23:27 -04:00
|
|
|
|
2013-08-31 11:13:08 -04:00
|
|
|
// bundle encrypted email together for sending
|
|
|
|
bundleEncryptedItems(email, encryptedList);
|
2013-08-27 12:23:27 -04:00
|
|
|
|
2013-08-31 11:13:08 -04:00
|
|
|
self.send(email, callback);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Encrypt an email symmetrically for a new user, write the secret one time key to the cloudstorage REST service, and send the email client side via SMTP.
|
|
|
|
*/
|
|
|
|
EmailDAO.prototype.encryptForNewUser = function(email, callback) {
|
|
|
|
var self = this,
|
|
|
|
ptItems = bundleForEncryption(email);
|
|
|
|
|
|
|
|
crypto.symEncryptList(ptItems, function(err, result) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
2013-08-29 08:28:26 -04:00
|
|
|
}
|
2013-08-28 08:12:39 -04:00
|
|
|
|
2013-08-31 11:13:08 -04:00
|
|
|
// bundle encrypted email together for sending
|
|
|
|
bundleEncryptedItems(email, result.list);
|
|
|
|
|
|
|
|
// TODO: write result.key to REST endpoint
|
|
|
|
|
2013-08-29 08:28:26 -04:00
|
|
|
self.send(email, callback);
|
|
|
|
});
|
|
|
|
};
|
2013-08-27 12:23:27 -04:00
|
|
|
|
2013-08-29 13:32:34 -04:00
|
|
|
/**
|
2013-08-31 11:13:08 -04:00
|
|
|
* Give the email a newly generated UUID, remove its attachments, and bundle all plaintext items to a batchable array for encryption.
|
|
|
|
*/
|
|
|
|
|
|
|
|
function bundleForEncryption(email) {
|
|
|
|
var ptItems = [email];
|
|
|
|
|
|
|
|
// generate a new UUID for the new email
|
|
|
|
email.id = util.UUID();
|
|
|
|
|
|
|
|
// add attachment to encryption batch and remove from email object
|
|
|
|
if (email.attachments) {
|
|
|
|
email.attachments.forEach(function(attachment) {
|
|
|
|
attachment.id = email.id;
|
|
|
|
ptItems.push(attachment);
|
|
|
|
});
|
|
|
|
delete email.attachments;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ptItems;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Frame the encrypted email message and append the encrypted attachments.
|
2013-08-29 13:32:34 -04:00
|
|
|
*/
|
2013-08-31 11:13:08 -04:00
|
|
|
|
|
|
|
function bundleEncryptedItems(email, encryptedList) {
|
|
|
|
var i;
|
|
|
|
|
|
|
|
// replace body and subject of the email with encrypted versions
|
|
|
|
email = frameEncryptedMessage(email, encryptedList[0]);
|
|
|
|
|
|
|
|
// add encrypted attachments
|
|
|
|
if (encryptedList.length > 1) {
|
|
|
|
email.attachments = [];
|
|
|
|
}
|
|
|
|
for (i = 1; i < encryptedList.length; i++) {
|
|
|
|
email.attachments.push({
|
|
|
|
fileName: 'Encrypted Attachment ' + i,
|
|
|
|
contentType: 'application/octet-stream',
|
|
|
|
uint8Array: util.binStr2Uint8Arr(JSON.stringify(encryptedList[i]))
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Frames an encrypted message in base64 Format.
|
|
|
|
*/
|
|
|
|
|
|
|
|
function frameEncryptedMessage(email, ct) {
|
2013-08-29 13:32:34 -04:00
|
|
|
var to, greeting, ctBase64;
|
|
|
|
|
2013-09-15 13:00:35 -04:00
|
|
|
var SUBJECT = str.subject,
|
|
|
|
MESSAGE = str.message + '\n\n\n',
|
|
|
|
PREFIX = str.cryptPrefix + '\n',
|
|
|
|
SUFFIX = '\n' + str.cryptSuffix,
|
|
|
|
SIGNATURE = '\n\n\n' + str.signature + '\n' + str.webSite + '\n\n';
|
|
|
|
|
2013-08-29 13:32:34 -04:00
|
|
|
// 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
|
|
|
|
ctBase64 = btoa(JSON.stringify(ct));
|
|
|
|
email.body = greeting + MESSAGE + PREFIX + ctBase64 + SUFFIX + SIGNATURE;
|
|
|
|
email.subject = SUBJECT;
|
|
|
|
|
|
|
|
return email;
|
2013-08-31 11:13:08 -04:00
|
|
|
}
|
2013-08-29 13:32:34 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Send an actual message object via smtp
|
|
|
|
*/
|
2013-08-29 08:28:26 -04:00
|
|
|
EmailDAO.prototype.send = function(email, callback) {
|
|
|
|
var self = this;
|
2013-08-27 12:23:27 -04:00
|
|
|
|
2013-08-29 08:28:26 -04:00
|
|
|
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;
|
|
|
|
|
2013-09-20 12:44:14 -04:00
|
|
|
self._imapClient.listAllFolders(callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the number of unread message for a folder
|
|
|
|
*/
|
|
|
|
EmailDAO.prototype.unreadMessages = function(path, callback) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
self._imapClient.unreadMessages(path, 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,
|
2013-08-28 13:20:59 -04:00
|
|
|
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-09-11 17:31:08 -04:00
|
|
|
function messageReady(err, gottenMessage) {
|
2013-08-21 07:43:19 -04:00
|
|
|
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;
|
2013-08-28 11:10:18 -04:00
|
|
|
|
2013-08-29 10:01:40 -04:00
|
|
|
// TODO: remove once attachments work again
|
|
|
|
if (itemCounter > 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-08-28 11:10:18 -04:00
|
|
|
// decrypt Message body
|
2013-09-15 13:00:35 -04:00
|
|
|
if (message.body.indexOf(str.cryptPrefix) !== -1 && message.body.indexOf(str.cryptSuffix) !== -1) {
|
2013-08-30 10:05:33 -04:00
|
|
|
decryptBody(message, function(err, ptMessage) {
|
2013-08-28 11:10:18 -04:00
|
|
|
message = ptMessage;
|
|
|
|
// return decrypted message
|
2013-08-30 10:05:33 -04:00
|
|
|
callback(err, message);
|
2013-08-28 11:10:18 -04:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// return unencrypted message
|
|
|
|
callback(null, message);
|
|
|
|
|
|
|
|
//check();
|
|
|
|
}
|
|
|
|
|
2013-08-30 10:05:33 -04:00
|
|
|
function decryptBody(email, callback) {
|
2013-09-15 13:00:35 -04:00
|
|
|
var ctMessageBase64, ctMessageJson, ctMessage, pubkeyIds;
|
2013-08-28 11:10:18 -04:00
|
|
|
|
|
|
|
// parse email body for encrypted message block
|
|
|
|
try {
|
2013-09-15 13:00:35 -04:00
|
|
|
// get base64 encoded message block
|
|
|
|
ctMessageBase64 = email.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0].trim();
|
|
|
|
// decode bae64
|
|
|
|
ctMessageJson = atob(ctMessageBase64);
|
|
|
|
// parse json string to get ciphertext object
|
|
|
|
ctMessage = JSON.parse(ctMessageJson);
|
2013-08-28 11:10:18 -04:00
|
|
|
} catch (e) {
|
|
|
|
callback({
|
|
|
|
errMsg: 'Error parsing encrypted message block!'
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// gather public key ids required to verify signatures
|
|
|
|
pubkeyIds = [{
|
|
|
|
_id: ctMessage.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.decryptListForUser([ctMessage], senderPubkeys, function(err, decryptedList) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var ptEmail = decryptedList[0];
|
|
|
|
email.body = ptEmail.body;
|
|
|
|
email.subject = ptEmail.subject;
|
|
|
|
|
|
|
|
callback(null, email);
|
|
|
|
});
|
|
|
|
});
|
2013-08-21 07:43:19 -04:00
|
|
|
}
|
|
|
|
|
2013-08-28 13:20: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;
|
|
|
|
// }
|
|
|
|
|
|
|
|
// // overwrite attachments array with the uint8array variant
|
|
|
|
// message.attachments = (attachments.length > 0) ? attachments : undefined;
|
|
|
|
// // cache message object in memory
|
|
|
|
// self.cacheItem(options.folder, message);
|
|
|
|
|
|
|
|
// callback(null, message);
|
|
|
|
// }
|
2013-08-21 07:43:19 -04:00
|
|
|
|
|
|
|
self._imapClient.getMessage({
|
|
|
|
path: options.folder,
|
2013-08-28 08:56:23 -04:00
|
|
|
uid: options.uid,
|
2013-09-11 17:31:08 -04:00
|
|
|
textOnly: true
|
|
|
|
}, messageReady);
|
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
|
|
|
};
|
|
|
|
|
|
|
|
return EmailDAO;
|
2013-08-28 04:31:53 -04:00
|
|
|
});
|