1
0
mirror of https://github.com/moparisthebest/mail synced 2025-02-26 07:41:48 -05:00

crypto error handling cleanup

This commit is contained in:
Tankred Hase 2013-08-30 16:05:33 +02:00
parent d1290e7a9f
commit 628cb0ddd9
6 changed files with 399 additions and 340 deletions

View File

@ -1,69 +1,91 @@
(function() { (function() {
'use strict'; 'use strict';
/** /**
* A Wrapper for Forge's AES-CBC encryption * A Wrapper for Forge's AES-CBC encryption
*/ */
var AesCBC = function(forge) { var AesCBC = function(forge) {
this._forge = forge;
};
var utl = forge.util; /**
* Encrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256)
* @param plaintext [String] The input string in UTF-16
* @param key [String] The base64 encoded key
* @param iv [String] The base64 encoded IV
* @return [String] The base64 encoded ciphertext
*/
AesCBC.prototype.encrypt = function(plaintext, key, iv) {
// validate args
if (!plaintext || !key || !iv) {
throw new Error("Missing args for encryption!");
}
/** // decode args to utf8 and encrypt
* Encrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256) var cipher = this._forge.aes.createEncryptionCipher(this._forge.util.decode64(key));
* @param plaintext [String] The input string in UTF-16 cipher.start(this._forge.util.decode64(iv));
* @param key [String] The base64 encoded key cipher.update(this._forge.util.createBuffer(this._forge.util.encodeUtf8(plaintext)));
* @param iv [String] The base64 encoded IV cipher.finish();
* @return [String] The base64 encoded ciphertext
*/
this.encrypt = function(plaintext, key, iv) {
// validate args
if (!plaintext || !key || !iv) {
throw new Error("Missing args for encryption!");
}
// decode args to utf8 and encrypt // encode to base64
var cipher = forge.aes.createEncryptionCipher(utl.decode64(key)); return this._forge.util.encode64(cipher.output.getBytes());
cipher.start(utl.decode64(iv)); };
cipher.update(utl.createBuffer(utl.encodeUtf8(plaintext)));
cipher.finish();
// encode to base64 /**
return utl.encode64(cipher.output.getBytes()); * Decrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256)
}; * @param ciphertext [String] The base64 encoded ciphertext
* @param key [String] The base64 encoded key
* @param iv [String] The base64 encoded IV
* @return [String] The decrypted plaintext in UTF-16
*/
AesCBC.prototype.decrypt = function(ciphertext, key, iv) {
// validate args
if (!ciphertext || !key || !iv) {
throw new Error("Missing args for decryption!");
}
/** // decode args input to utf8 decrypt
* Decrypt a String using AES-CBC-Pkcs7 using the provided keysize (e.g. 128, 256) var cipher = this._forge.aes.createDecryptionCipher(this._forge.util.decode64(key));
* @param ciphertext [String] The base64 encoded ciphertext cipher.start(this._forge.util.decode64(iv));
* @param key [String] The base64 encoded key cipher.update(this._forge.util.createBuffer(this._forge.util.decode64(ciphertext)));
* @param iv [String] The base64 encoded IV cipher.finish();
* @return [String] The decrypted plaintext in UTF-16
*/
this.decrypt = function(ciphertext, key, iv) {
// validate args
if (!ciphertext || !key || !iv) {
throw new Error("Missing args for decryption!");
}
// decode args input to utf8 decrypt // decode to utf16
var cipher = forge.aes.createDecryptionCipher(utl.decode64(key)); return this._forge.util.decodeUtf8(cipher.output.getBytes());
cipher.start(utl.decode64(iv)); };
cipher.update(utl.createBuffer(utl.decode64(ciphertext)));
cipher.finish();
// decode to utf16 /**
return utl.decodeUtf8(cipher.output.getBytes()); * Calculate a hmac using SHA-256 for a given input
}; * @param parts [Array] Array of Base64 encoded parts
* @param key [String] The base64 encoded key
* @return [String] The Base64 encoded hmac
*/
AesCBC.prototype.hmac = function(parts, key) {
var self = this;
}; // validate args
if (!parts || !key) {
throw new Error("Missing args for hmac processing!");
}
if (typeof define !== 'undefined' && define.amd) { var hmac = self._forge.hmac.create();
// AMD hmac.start('sha256', self._forge.util.decode64(key));
define(['forge'], function(forge) { parts.forEach(function(i) {
return new AesCBC(forge); // decode base64 part and append to hmac msg
}); hmac.update(self._forge.util.decode64(i));
} else if (typeof module !== 'undefined' && module.exports) { });
// node.js
module.exports = new AesCBC(require('node-forge')); return self._forge.util.encode64(hmac.digest().getBytes());
} };
if (typeof define !== 'undefined' && define.amd) {
// AMD
define(['forge'], function(forge) {
return new AesCBC(forge);
});
} else if (typeof module !== 'undefined' && module.exports) {
// node.js
module.exports = new AesCBC(require('node-forge'));
}
})(); })();

View File

@ -17,27 +17,16 @@
require(['cryptoLib/crypto-batch'], function(batch) { require(['cryptoLib/crypto-batch'], function(batch) {
var i = e.data, var output;
output = null;
if (i.type === 'encrypt' && i.receiverPubkeys && i.senderPrivkey && i.list) { try {
// start encryption output = doOperation(batch, e.data);
output = batch.encryptListForUser(i.list, i.receiverPubkeys, i.senderPrivkey); } catch (e) {
output = {
} else if (i.type === 'decrypt' && i.senderPubkeys && i.receiverPrivkey && i.list) { err: {
// start decryption errMsg: (e.message) ? e.message : e
output = batch.decryptListForUser(i.list, i.senderPubkeys, i.receiverPrivkey); }
};
} else if (i.type === 'reencrypt' && i.senderPubkeys && i.receiverPrivkey && i.list && i.symKey) {
// start validation and re-encryption
output = batch.reencryptListKeysForUser(i.list, i.senderPubkeys, i.receiverPrivkey, i.symKey);
} else if (i.type === 'decryptItems' && i.symKey && i.list) {
// start decryption
output = batch.decryptKeysAndList(i.list, i.symKey);
} else {
throw 'Not all arguments for web worker crypto are defined!';
} }
// pass output back to main thread // pass output back to main thread
@ -47,4 +36,34 @@
}); });
}; };
function doOperation(batch, i) {
var output;
if (i.type === 'encrypt' && i.receiverPubkeys && i.senderPrivkey && i.list) {
// start encryption
output = batch.encryptListForUser(i.list, i.receiverPubkeys, i.senderPrivkey);
} else if (i.type === 'decrypt' && i.senderPubkeys && i.receiverPrivkey && i.list) {
// start decryption
output = batch.decryptListForUser(i.list, i.senderPubkeys, i.receiverPrivkey);
} else if (i.type === 'reencrypt' && i.senderPubkeys && i.receiverPrivkey && i.list && i.symKey) {
// start validation and re-encryption
output = batch.reencryptListKeysForUser(i.list, i.senderPubkeys, i.receiverPrivkey, i.symKey);
} else if (i.type === 'decryptItems' && i.symKey && i.list) {
// start decryption
output = batch.decryptKeysAndList(i.list, i.symKey);
} else {
output = {
err: {
errMsg: 'Not all arguments for web worker crypto are defined!'
}
};
}
return output;
}
}()); }());

View File

@ -28,7 +28,7 @@
// set sender's keypair id for later verification // set sender's keypair id for later verification
i.senderPk = senderKeyId; i.senderPk = senderKeyId;
// sign the bundle // sign the bundle
i.signature = rsa.sign([i.iv, util.str2Base64(i.id), util.str2Base64(i.senderPk), i.encryptedKey, i.ciphertext]); i.signature = rsa.sign([i.iv, util.str2Base64(i.senderPk), i.encryptedKey, i.ciphertext]);
// delete plaintext values // delete plaintext values
delete i.plaintext; delete i.plaintext;
@ -79,7 +79,7 @@
rsa.init(senderPubkey); rsa.init(senderPubkey);
// verify signature // verify signature
if (!rsa.verify([i.iv, util.str2Base64(i.id), util.str2Base64(i.senderPk), i.encryptedKey, i.ciphertext], i.signature)) { if (!rsa.verify([i.iv, util.str2Base64(i.senderPk), i.encryptedKey, i.ciphertext], i.signature)) {
throw new Error('Verifying RSA signature failed!'); throw new Error('Verifying RSA signature failed!');
} }
// decrypt symmetric item key for user // decrypt symmetric item key for user

View File

@ -2,305 +2,322 @@
* High level crypto api that invokes native crypto (if available) and * High level crypto api that invokes native crypto (if available) and
* gracefully degrades to JS crypto (if unavailable) * gracefully degrades to JS crypto (if unavailable)
*/ */
define(['cryptoLib/util', 'cryptoLib/aes-cbc', 'cryptoLib/rsa', 'cryptoLib/crypto-batch', define(function(require) {
'js/crypto/pbkdf2', 'js/app-config' 'use strict';
], function(util, aes, rsa, cryptoBatch, pbkdf2, app) {
'use strict';
var self = {}; var util = require('cryptoLib/util'),
aes = require('cryptoLib/aes-cbc'),
rsa = require('cryptoLib/rsa'),
cryptoBatch = require('cryptoLib/crypto-batch'),
pbkdf2 = require('js/crypto/pbkdf2'),
app = require('js/app-config');
var passBasedKey; var self = {},
passBasedKey;
/** /**
* Initializes the crypto modules by fetching the user's * Initializes the crypto modules by fetching the user's
* encrypted secret key from storage and storing it in memory. * encrypted secret key from storage and storing it in memory.
*/ */
self.init = function(args, callback) { self.init = function(args, callback) {
// valdiate input // valdiate input
if (!args.emailAddress || !args.keySize || !args.rsaKeySize) { if (!args.emailAddress || !args.keySize || !args.rsaKeySize) {
callback({ callback({
errMsg: 'Crypto init failed. Not all args set!' errMsg: 'Crypto init failed. Not all args set!'
}); });
return; return;
} }
self.emailAddress = args.emailAddress; self.emailAddress = args.emailAddress;
self.keySize = args.keySize; self.keySize = args.keySize;
self.ivSize = args.keySize; self.ivSize = args.keySize;
self.rsaKeySize = args.rsaKeySize; self.rsaKeySize = args.rsaKeySize;
// derive PBKDF2 from password in web worker thread // derive PBKDF2 from password in web worker thread
self.deriveKey(args.password, self.keySize, function(err, derivedKey) { self.deriveKey(args.password, self.keySize, function(err, derivedKey) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
// remember pbkdf2 for later use // remember pbkdf2 for later use
passBasedKey = derivedKey; passBasedKey = derivedKey;
// check if key exists // check if key exists
if (!args.storedKeypair) { if (!args.storedKeypair) {
// generate keys, encrypt and persist if none exists // generate keys, encrypt and persist if none exists
generateKeypair(derivedKey); generateKeypair(derivedKey);
} else { } else {
// decrypt key // decrypt key
decryptKeypair(args.storedKeypair, derivedKey); decryptKeypair(args.storedKeypair, derivedKey);
} }
}); });
function generateKeypair(derivedKey) { function generateKeypair(derivedKey) {
// generate RSA keypair in web worker // generate RSA keypair in web worker
rsa.generateKeypair(self.rsaKeySize, function(err, generatedKeypair) { rsa.generateKeypair(self.rsaKeySize, function(err, generatedKeypair) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
// encrypt keypair // encrypt keypair
var iv = util.random(self.ivSize); var iv = util.random(self.ivSize);
var encryptedPrivateKey = aes.encrypt(generatedKeypair.privkeyPem, derivedKey, iv); var encryptedPrivateKey = aes.encrypt(generatedKeypair.privkeyPem, derivedKey, iv);
// new encrypted keypair object // new encrypted keypair object
var newKeypair = { var newKeypair = {
publicKey: { publicKey: {
_id: generatedKeypair._id, _id: generatedKeypair._id,
userId: self.emailAddress, userId: self.emailAddress,
publicKey: generatedKeypair.pubkeyPem publicKey: generatedKeypair.pubkeyPem
}, },
privateKey: { privateKey: {
_id: generatedKeypair._id, _id: generatedKeypair._id,
userId: self.emailAddress, userId: self.emailAddress,
encryptedKey: encryptedPrivateKey, encryptedKey: encryptedPrivateKey,
iv: iv iv: iv
} }
}; };
// return generated keypair for storage in keychain dao // return generated keypair for storage in keychain dao
callback(null, newKeypair); callback(null, newKeypair);
}); });
} }
function decryptKeypair(storedKeypair, derivedKey) { function decryptKeypair(storedKeypair, derivedKey) {
var decryptedPrivateKey; var decryptedPrivateKey;
// validate input // validate input
if (!storedKeypair || !storedKeypair.privateKey || !storedKeypair.privateKey.encryptedKey || !storedKeypair.privateKey.iv) { if (!storedKeypair || !storedKeypair.privateKey || !storedKeypair.privateKey.encryptedKey || !storedKeypair.privateKey.iv) {
callback({ callback({
errMsg: 'Incomplete arguments for private key decryption!' errMsg: 'Incomplete arguments for private key decryption!'
}); });
return; return;
} }
// try to decrypt with derivedKey // try to decrypt with derivedKey
try { try {
var prK = storedKeypair.privateKey; var prK = storedKeypair.privateKey;
decryptedPrivateKey = aes.decrypt(prK.encryptedKey, derivedKey, prK.iv); decryptedPrivateKey = aes.decrypt(prK.encryptedKey, derivedKey, prK.iv);
} catch (ex) { } catch (ex) {
callback({ callback({
errMsg: 'Wrong password!' errMsg: 'Wrong password!'
}); });
return; return;
} }
// set rsa keys // set rsa keys
rsa.init(storedKeypair.publicKey.publicKey, decryptedPrivateKey, storedKeypair.publicKey._id); rsa.init(storedKeypair.publicKey.publicKey, decryptedPrivateKey, storedKeypair.publicKey._id);
callback(); callback();
} }
}; };
/** /**
* Do PBKDF2 key derivation in a WebWorker thread * Do PBKDF2 key derivation in a WebWorker thread
*/ */
self.deriveKey = function(password, keySize, callback) { self.deriveKey = function(password, keySize, callback) {
startWorker('/crypto/pbkdf2-worker.js', { startWorker('/crypto/pbkdf2-worker.js', {
password: password, password: password,
keySize: keySize keySize: keySize
}, callback, function() { }, callback, function() {
return pbkdf2.getKey(password, keySize); return pbkdf2.getKey(password, keySize);
}); });
}; };
// //
// En/Decrypts single item // En/Decrypts single item
// //
self.aesEncrypt = function(plaintext, key, iv, callback) { self.aesEncrypt = function(plaintext, key, iv, callback) {
startWorker('/crypto/aes-worker.js', { startWorker('/crypto/aes-worker.js', {
type: 'encrypt', type: 'encrypt',
plaintext: plaintext, plaintext: plaintext,
key: key, key: key,
iv: iv iv: iv
}, callback, function() { }, callback, function() {
return self.aesEncryptSync(plaintext, key, iv); return self.aesEncryptSync(plaintext, key, iv);
}); });
}; };
self.aesDecrypt = function(ciphertext, key, iv, callback) { self.aesDecrypt = function(ciphertext, key, iv, callback) {
startWorker('/crypto/aes-worker.js', { startWorker('/crypto/aes-worker.js', {
type: 'decrypt', type: 'decrypt',
ciphertext: ciphertext, ciphertext: ciphertext,
key: key, key: key,
iv: iv iv: iv
}, callback, function() { }, callback, function() {
return self.aesDecryptSync(ciphertext, key, iv); return self.aesDecryptSync(ciphertext, key, iv);
}); });
}; };
self.aesEncryptSync = function(plaintext, key, iv) { self.aesEncryptSync = function(plaintext, key, iv) {
return aes.encrypt(plaintext, key, iv); return aes.encrypt(plaintext, key, iv);
}; };
self.aesDecryptSync = function(ciphertext, key, iv) { self.aesDecryptSync = function(ciphertext, key, iv) {
return aes.decrypt(ciphertext, key, iv); return aes.decrypt(ciphertext, key, iv);
}; };
// //
// En/Decrypt a list of items with AES in a WebWorker thread // En/Decrypt a list of items with AES in a WebWorker thread
// //
self.aesEncryptList = function(list, callback) { self.aesEncryptList = function(list, callback) {
startWorker('/crypto/aes-batch-worker.js', { startWorker('/crypto/aes-batch-worker.js', {
type: 'encrypt', type: 'encrypt',
list: list list: list
}, callback, function() { }, callback, function() {
return cryptoBatch.encryptList(list); return cryptoBatch.encryptList(list);
}); });
}; };
self.aesDecryptList = function(list, callback) { self.aesDecryptList = function(list, callback) {
startWorker('/crypto/aes-batch-worker.js', { startWorker('/crypto/aes-batch-worker.js', {
type: 'decrypt', type: 'decrypt',
list: list list: list
}, callback, function() { }, callback, function() {
return cryptoBatch.decryptList(list); return cryptoBatch.decryptList(list);
}); });
}; };
// //
// En/Decrypt something speficially using the user's secret key // En/Decrypt something speficially using the user's secret key
// //
self.encryptListForUser = function(list, receiverPubkeys, callback) { self.encryptListForUser = function(list, receiverPubkeys, callback) {
var envelope, envelopes = []; var envelope, envelopes = [];
if (!receiverPubkeys || receiverPubkeys.length !== 1) { if (!receiverPubkeys || receiverPubkeys.length !== 1) {
callback({ callback({
errMsg: 'Encryption is currently implemented for only one receiver!' errMsg: 'Encryption is currently implemented for only one receiver!'
}); });
return; return;
} }
var keypair = rsa.exportKeys(); var keypair = rsa.exportKeys();
var senderPrivkey = { var senderPrivkey = {
_id: keypair._id, _id: keypair._id,
privateKey: keypair.privkeyPem privateKey: keypair.privkeyPem
}; };
// package objects into batchable envelope format // package objects into batchable envelope format
list.forEach(function(i) { list.forEach(function(i) {
envelope = { envelope = {
id: i.id, id: i.id,
plaintext: i, plaintext: i,
key: util.random(self.keySize), key: util.random(self.keySize),
iv: util.random(self.ivSize), iv: util.random(self.ivSize),
receiverPk: receiverPubkeys[0]._id receiverPk: receiverPubkeys[0]._id
}; };
envelopes.push(envelope); envelopes.push(envelope);
}); });
startWorker('/crypto/crypto-batch-worker.js', { startWorker('/crypto/crypto-batch-worker.js', {
type: 'encrypt', type: 'encrypt',
list: envelopes, list: envelopes,
senderPrivkey: senderPrivkey, senderPrivkey: senderPrivkey,
receiverPubkeys: receiverPubkeys receiverPubkeys: receiverPubkeys
}, callback, function() { }, callback, function() {
return cryptoBatch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey); return cryptoBatch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey);
}); });
}; };
self.decryptListForUser = function(list, senderPubkeys, callback) { self.decryptListForUser = function(list, senderPubkeys, callback) {
if (!senderPubkeys || senderPubkeys < 1) { if (!senderPubkeys || senderPubkeys < 1) {
callback({ callback({
errMsg: 'Sender public keys must be set!' errMsg: 'Sender public keys must be set!'
}); });
return; return;
} }
var keypair = rsa.exportKeys(); var keypair = rsa.exportKeys();
var receiverPrivkey = { var receiverPrivkey = {
_id: keypair._id, _id: keypair._id,
privateKey: keypair.privkeyPem privateKey: keypair.privkeyPem
}; };
startWorker('/crypto/crypto-batch-worker.js', { startWorker('/crypto/crypto-batch-worker.js', {
type: 'decrypt', type: 'decrypt',
list: list, list: list,
receiverPrivkey: receiverPrivkey, receiverPrivkey: receiverPrivkey,
senderPubkeys: senderPubkeys senderPubkeys: senderPubkeys
}, callback, function() { }, callback, function() {
return cryptoBatch.decryptListForUser(list, senderPubkeys, receiverPrivkey); return cryptoBatch.decryptListForUser(list, senderPubkeys, receiverPrivkey);
}); });
}; };
// //
// Re-encrypt keys item and items seperately // Re-encrypt keys item and items seperately
// //
self.reencryptListKeysForUser = function(list, senderPubkeys, callback) { self.reencryptListKeysForUser = function(list, senderPubkeys, callback) {
var keypair = rsa.exportKeys(); var keypair = rsa.exportKeys();
var receiverPrivkey = { var receiverPrivkey = {
_id: keypair._id, _id: keypair._id,
privateKey: keypair.privkeyPem privateKey: keypair.privkeyPem
}; };
startWorker('/crypto/crypto-batch-worker.js', { startWorker('/crypto/crypto-batch-worker.js', {
type: 'reencrypt', type: 'reencrypt',
list: list, list: list,
receiverPrivkey: receiverPrivkey, receiverPrivkey: receiverPrivkey,
senderPubkeys: senderPubkeys, senderPubkeys: senderPubkeys,
symKey: passBasedKey symKey: passBasedKey
}, callback, function() { }, callback, function() {
return cryptoBatch.reencryptListKeysForUser(list, senderPubkeys, receiverPrivkey, passBasedKey); return cryptoBatch.reencryptListKeysForUser(list, senderPubkeys, receiverPrivkey, passBasedKey);
}); });
}; };
self.decryptKeysAndList = function(list, callback) { self.decryptKeysAndList = function(list, callback) {
startWorker('/crypto/crypto-batch-worker.js', { startWorker('/crypto/crypto-batch-worker.js', {
type: 'decryptItems', type: 'decryptItems',
list: list, list: list,
symKey: passBasedKey symKey: passBasedKey
}, callback, function() { }, callback, function() {
return cryptoBatch.decryptKeysAndList(list, passBasedKey); return cryptoBatch.decryptKeysAndList(list, passBasedKey);
}); });
}; };
// //
// helper functions // helper functions
// //
function startWorker(script, args, callback, noWorker) { function startWorker(script, args, callback, noWorker) {
// check for WebWorker support // check for WebWorker support
if (window.Worker) { if (window.Worker) {
// init webworker thread // init webworker thread
var worker = new Worker(app.config.workerPath + script); var worker = new Worker(app.config.workerPath + script);
worker.onmessage = function(e) { worker.onmessage = function(e) {
// return derived key from the worker if (e.data.err) {
callback(null, e.data); callback(e.data.err);
}; return;
}
// return result from the worker
callback(null, e.data);
};
// send data to the worker // send data to the worker
worker.postMessage(args); worker.postMessage(args);
} else { } else {
// no WebWorker support... do synchronous call // no WebWorker support... do synchronous call
var result = noWorker(); var result;
callback(null, result); try {
} result = noWorker();
} } catch (e) {
callback({
errMsg: (e.message) ? e.message : e
});
return;
}
return self; callback(null, result);
}
}
return self;
}); });

View File

@ -298,7 +298,7 @@ define(function(require) {
/* message was not found in cache... fetch from imap server */ /* message was not found in cache... fetch from imap server */
function messageReady(err, gottenMessage) { function bodyReady(err, gottenMessage) {
message = gottenMessage; message = gottenMessage;
itemCounter++; itemCounter++;
// remember how many items should be fetched before the callback fires // remember how many items should be fetched before the callback fires
@ -311,10 +311,10 @@ define(function(require) {
// decrypt Message body // decrypt Message body
if (message.body.indexOf(PREFIX) !== -1 && message.body.indexOf(SUFFIX) !== -1) { if (message.body.indexOf(PREFIX) !== -1 && message.body.indexOf(SUFFIX) !== -1) {
decryptMessageBody(message, function(err, ptMessage) { decryptBody(message, function(err, ptMessage) {
message = ptMessage; message = ptMessage;
// return decrypted message // return decrypted message
callback(null, message); callback(err, message);
}); });
return; return;
} }
@ -325,7 +325,7 @@ define(function(require) {
//check(); //check();
} }
function decryptMessageBody(email, callback) { function decryptBody(email, callback) {
var ctMessageBase64, ctMessage, pubkeyIds; var ctMessageBase64, ctMessage, pubkeyIds;
// parse email body for encrypted message block // parse email body for encrypted message block
@ -390,7 +390,8 @@ define(function(require) {
self._imapClient.getMessage({ self._imapClient.getMessage({
path: options.folder, path: options.folder,
uid: options.uid, uid: options.uid,
onMessage: messageReady, onBody: bodyReady,
onEnd: bodyReady
/*onAttachment: attachmentReady*/ /*onAttachment: attachmentReady*/
}); });
}; };

View File

@ -272,7 +272,7 @@ define(function(require) {
it('should parse message body without attachement', function(done) { it('should parse message body without attachement', function(done) {
var uid = 415; var uid = 415;
imapClientStub.getMessage.yieldsTo('onMessage', null, { imapClientStub.getMessage.yieldsTo('onBody', null, {
uid: uid, uid: uid,
body: '' body: ''
}); });