Merge pull request #255 from whiteout-io/dev/WO-851

[WO-851] Request OAuth token on each connection request
This commit is contained in:
Tankred Hase 2015-02-02 16:50:12 +01:00
commit 038437595e
7 changed files with 222 additions and 221 deletions

View File

@ -4,12 +4,9 @@ var ngModule = angular.module('woEmail');
ngModule.service('account', Account); ngModule.service('account', Account);
module.exports = Account; module.exports = Account;
var axe = require('axe-logger'), var util = require('crypto-lib').util;
util = require('crypto-lib').util,
PgpMailer = require('pgpmailer'),
ImapClient = require('imap-client');
function Account(appConfig, auth, accountStore, email, outbox, keychain, updateHandler, pgpbuilder, dialog) { function Account(appConfig, auth, accountStore, email, outbox, keychain, updateHandler, dialog) {
this._appConfig = appConfig; this._appConfig = appConfig;
this._auth = auth; this._auth = auth;
this._accountStore = accountStore; this._accountStore = accountStore;
@ -17,7 +14,6 @@ function Account(appConfig, auth, accountStore, email, outbox, keychain, updateH
this._outbox = outbox; this._outbox = outbox;
this._keychain = keychain; this._keychain = keychain;
this._updateHandler = updateHandler; this._updateHandler = updateHandler;
this._pgpbuilder = pgpbuilder;
this._dialog = dialog; this._dialog = dialog;
this._accounts = []; // init accounts list this._accounts = []; // init accounts list
} }
@ -102,68 +98,16 @@ Account.prototype.init = function(options) {
}); });
}; };
/**
* Check if the user agent is online.
*/
Account.prototype.isOnline = function() {
return navigator.onLine;
};
/** /**
* Event that is called when the user agent goes online. This create new instances of the imap-client and pgp-mailer and connects to the mail server. * Event that is called when the user agent goes online. This create new instances of the imap-client and pgp-mailer and connects to the mail server.
*/ */
Account.prototype.onConnect = function(callback) { Account.prototype.onConnect = function(callback) {
var self = this; if (!this._emailDao || !this._emailDao._account) {
var config = self._appConfig.config;
callback = callback || self._dialog.error;
if (!self.isOnline() || !self._emailDao || !self._emailDao._account) {
// prevent connection infinite loop // prevent connection infinite loop
return; return;
} }
// init imap/smtp clients this._emailDao.onConnect().then(callback).catch(callback);
self._auth.getCredentials().then(function(credentials) {
// add the maximum update batch size for imap folders to the imap configuration
credentials.imap.maxUpdateSize = config.imapUpdateBatchSize;
// tls socket worker path for multithreaded tls in non-native tls environments
credentials.imap.tlsWorkerPath = credentials.smtp.tlsWorkerPath = config.workerPath + '/tcp-socket-tls-worker.min.js';
var pgpMailer = new PgpMailer(credentials.smtp, self._pgpbuilder);
var imapClient = new ImapClient(credentials.imap);
imapClient.onError = onConnectionError;
pgpMailer.onError = onConnectionError;
// certificate update handling
imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect.bind(self), self._dialog.error);
pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect.bind(self), self._dialog.error);
// connect to clients
return self._emailDao.onConnect({
imapClient: imapClient,
pgpMailer: pgpMailer,
ignoreUploadOnSent: self._emailDao.checkIgnoreUploadOnSent(credentials.imap.host)
});
}).then(callback).catch(callback);
function onConnectionError(error) {
axe.debug('Connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : ''));
setTimeout(function() {
axe.debug('Reconnecting...');
// re-init client modules on error
self.onConnect(function(err) {
if (err) {
axe.error('Reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : ''));
return;
}
axe.debug('Reconnect attempt complete.');
});
}, config.reconnectInterval);
}
}; };
/** /**

View File

@ -5,7 +5,10 @@ ngModule.service('email', Email);
module.exports = Email; module.exports = Email;
var config = require('../app-config').config, var config = require('../app-config').config,
str = require('../app-config').string; str = require('../app-config').string,
axe = require('axe-logger'),
PgpMailer = require('pgpmailer'),
ImapClient = require('imap-client');
// //
// //
@ -50,13 +53,15 @@ var MSG_PART_TYPE_HTML = 'html';
* @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages
* @param {Object} mailreader Parses MIME messages received from IMAP * @param {Object} mailreader Parses MIME messages received from IMAP
*/ */
function Email(keychain, pgp, accountStore, pgpbuilder, mailreader, dialog) { function Email(keychain, pgp, accountStore, pgpbuilder, mailreader, dialog, appConfig, auth) {
this._keychain = keychain; this._keychain = keychain;
this._pgp = pgp; this._pgp = pgp;
this._devicestorage = accountStore; this._devicestorage = accountStore;
this._pgpbuilder = pgpbuilder; this._pgpbuilder = pgpbuilder;
this._mailreader = mailreader; this._mailreader = mailreader;
this._dialog = dialog; this._dialog = dialog;
this._appConfig = appConfig;
this._auth = auth;
} }
@ -896,37 +901,40 @@ Email.prototype.decryptBody = function(options) {
* Encrypted (if necessary) and sends a message with a predefined clear text greeting. * Encrypted (if necessary) and sends a message with a predefined clear text greeting.
* *
* @param {Object} options.email The message to be sent * @param {Object} options.email The message to be sent
* @param {Object} mailer an instance of the pgpmailer to be used for testing purposes only
*/ */
Email.prototype.sendEncrypted = function(options) { Email.prototype.sendEncrypted = function(options, mailer) {
// mime encode, sign, encrypt and send email via smtp // mime encode, sign, encrypt and send email via smtp
return this._sendGeneric({ return this._sendGeneric({
encrypt: true, encrypt: true,
smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage
mail: options.email, mail: options.email,
publicKeysArmored: options.email.publicKeysArmored publicKeysArmored: options.email.publicKeysArmored
}); }, mailer);
}; };
/** /**
* Sends a signed message in the plain * Sends a signed message in the plain
* *
* @param {Object} options.email The message to be sent * @param {Object} options.email The message to be sent
* @param {Object} mailer an instance of the pgpmailer to be used for testing purposes only
*/ */
Email.prototype.sendPlaintext = function(options) { Email.prototype.sendPlaintext = function(options, mailer) {
// add suffix to plaintext mail // add suffix to plaintext mail
options.email.body += str.signature + config.cloudUrl + '/' + this._account.emailAddress; options.email.body += str.signature + config.cloudUrl + '/' + this._account.emailAddress;
// mime encode, sign and send email via smtp // mime encode, sign and send email via smtp
return this._sendGeneric({ return this._sendGeneric({
smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage
mail: options.email mail: options.email
}); }, mailer);
}; };
/** /**
* This funtion wraps error handling for sending via pgpMailer and uploading to imap. * This funtion wraps error handling for sending via pgpMailer and uploading to imap.
* @param {Object} options.email The message to be sent * @param {Object} options.email The message to be sent
* @param {Object} mailer an instance of the pgpmailer to be used for testing purposes only
*/ */
Email.prototype._sendGeneric = function(options) { Email.prototype._sendGeneric = function(options, mailer) {
var self = this; var self = this;
self.busy(); self.busy();
return new Promise(function(resolve) { return new Promise(function(resolve) {
@ -934,8 +942,33 @@ Email.prototype._sendGeneric = function(options) {
resolve(); resolve();
}).then(function() { }).then(function() {
return self._mailerSend(options); // get the smtp credentials
return self._auth.getCredentials();
}).then(function(credentials) {
// gmail does not require you to upload to the sent items folder after successful sending, whereas most other providers do
self.ignoreUploadOnSent = self.checkIgnoreUploadOnSent(credentials.smtp.host);
// tls socket worker path for multithreaded tls in non-native tls environments
credentials.smtp.tlsWorkerPath = config.workerPath + '/tcp-socket-tls-worker.min.js';
// create a new pgpmailer
self._pgpMailer = (mailer || new PgpMailer(credentials.smtp, self._pgpbuilder));
// certificate update retriggers sending after cert update is persisted
self._pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self._sendGeneric.bind(self, options), self._dialog.error);
}).then(function() {
// send the email
return new Promise(function(resolve, reject) {
self._pgpMailer.send(options, function(err, rfcText) {
if (err) {
reject(err);
} else {
resolve(rfcText);
}
});
});
}).then(function(rfcText) { }).then(function(rfcText) {
// try to upload to sent, but we don't actually care if the upload failed or not // try to upload to sent, but we don't actually care if the upload failed or not
// this should not negatively impact the process of sending // this should not negatively impact the process of sending
@ -987,29 +1020,45 @@ Email.prototype.encrypt = function(options) {
* given instance of the imap client. If the connection attempt was successful, it will * 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. * 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} imap an instance of the imap-client to be used for testing purposes only
* @param {Object} options.pgpMailer The SMTP client used to send messages
*/ */
Email.prototype.onConnect = function(options) { Email.prototype.onConnect = function(imap) {
var self = this; var self = this;
self._account.loggingIn = true; self._account.loggingIn = true;
self._imapClient = options.imapClient; // init imap/smtp clients
self._pgpMailer = options.pgpMailer; return self._auth.getCredentials().then(function(credentials) {
// add the maximum update batch size for imap folders to the imap configuration
credentials.imap.maxUpdateSize = config.imapUpdateBatchSize;
// gmail does not require you to upload to the sent items folder after successful sending, whereas most other providers do // tls socket worker path for multithreaded tls in non-native tls environments
self.ignoreUploadOnSent = !!options.ignoreUploadOnSent; credentials.imap.tlsWorkerPath = config.workerPath + '/tcp-socket-tls-worker.min.js';
return imapLogin().then(function() { self._imapClient = (imap || new ImapClient(credentials.imap));
self._imapClient.onError = onConnectionError; // connection error handling
self._imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect.bind(self), self._dialog.error); // certificate update handling
self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self); // attach sync update handler
}).then(function() {
// imap login
return new Promise(function(resolve, reject) {
self._imapClient.login(function(err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}).then(function() {
self._account.loggingIn = false; self._account.loggingIn = false;
// init folders // init folders
return self._initFoldersFromImap(); return self._initFoldersFromImap();
}).then(function() { }).then(function() {
// attach sync update handler
self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self);
// fill the imap mailboxCache with information we have locally available: // fill the imap mailboxCache with information we have locally available:
// - highest locally available moseq (NB! JavaScript can't handle 64 bit uints, so modseq values are strings) // - highest locally available moseq (NB! JavaScript can't handle 64 bit uints, so modseq values are strings)
// - list of locally available uids // - list of locally available uids
@ -1071,16 +1120,20 @@ Email.prototype.onConnect = function(options) {
}); });
}); });
function imapLogin() { function onConnectionError(error) {
return new Promise(function(resolve, reject) { axe.debug('IMAP connection error, disconnected. Reason: ' + error.message + (error.stack ? ('\n' + error.stack) : ''));
self._imapClient.login(function(err) {
if (err) { if (!self.isOnline()) {
reject(err); return;
} else { }
resolve();
} axe.debug('Attempting reconnect in ' + config.reconnectInterval / 1000 + ' seconds.');
});
}); setTimeout(function() {
axe.debug('Reconnecting the IMAP stack');
// re-init client modules on error
self.onConnect().catch(self._dialog.error);
}, config.reconnectInterval);
} }
}; };
@ -1667,24 +1720,6 @@ Email.prototype._parse = function(options) {
}); });
}; };
/**
* Send email via smtp
* @param {Object} options The options to be passed to the pgpMailer
* @return {Promise}
*/
Email.prototype._mailerSend = function(options) {
var self = this;
return new Promise(function(resolve, reject) {
self._pgpMailer.send(options, function(err, rfcText) {
if (err) {
reject(err);
} else {
resolve(rfcText);
}
});
});
};
/** /**
* Uploads a message to the sent folder, if necessary. * Uploads a message to the sent folder, if necessary.
* Calls back immediately if ignoreUploadOnSent == true or not sent folder was found. * Calls back immediately if ignoreUploadOnSent == true or not sent folder was found.
@ -1758,6 +1793,13 @@ Email.prototype.checkIgnoreUploadOnSent = function(hostname) {
}; };
/**
* Check if the user agent is online.
*/
Email.prototype.isOnline = function() {
return navigator.onLine;
};
// //
// //
// Helper Functions // Helper Functions

View File

@ -240,16 +240,8 @@ Auth.prototype.useOAuth = function(hostname) {
Auth.prototype.getOAuthToken = function() { Auth.prototype.getOAuthToken = function() {
var self = this; var self = this;
if (self.oauthToken) { // get a fresh oauth token
// removed cached token and get a new one return self._oauth.getOAuthToken(self.emailAddress).then(onToken);
return self._oauth.refreshToken({
emailAddress: self.emailAddress,
oldToken: self.oauthToken
}).then(onToken);
} else {
// get a fresh oauth token
return self._oauth.getOAuthToken(self.emailAddress).then(onToken);
}
function onToken(oauthToken) { function onToken(oauthToken) {
// shortcut if the email address is already known // shortcut if the email address is already known
@ -317,7 +309,7 @@ Auth.prototype._loadCredentials = function() {
* @param {Function} callback The error handler * @param {Function} callback The error handler
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate * @param {[type]} pemEncodedCert The PEM encoded SSL certificate
*/ */
Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) { Auth.prototype.handleCertificateUpdate = function(component, reconnectCallback, callback, pemEncodedCert) {
var self = this; var self = this;
axe.debug('new ssl certificate received: ' + pemEncodedCert); axe.debug('new ssl certificate received: ' + pemEncodedCert);
@ -351,7 +343,7 @@ Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback
self[component].ca = pemEncodedCert; self[component].ca = pemEncodedCert;
self.credentialsDirty = true; self.credentialsDirty = true;
self.storeCredentials().then(function() { self.storeCredentials().then(function() {
onConnect(callback); reconnectCallback(callback);
}).catch(callback); }).catch(callback);
} }
}); });

View File

@ -14,7 +14,7 @@ var ImapClient = require('imap-client'),
describe('Email DAO integration tests', function() { describe('Email DAO integration tests', function() {
this.timeout(100000); this.timeout(100000);
var accountService, emailDao, imapClient, imapMessages, imapFolders, imapServer, smtpServer, smtpClient, userStorage, auth, var accountService, emailDao, imapClient, pgpMailer, imapMessages, imapFolders, imapServer, smtpServer, smtpClient, userStorage, auth,
mockKeyPair, inbox, spam; mockKeyPair, inbox, spam;
var testAccount = { var testAccount = {
@ -209,6 +209,7 @@ describe('Email DAO integration tests', function() {
}); });
function initAccountService() { function initAccountService() {
// create imap/smtp clients with stubbed tcp sockets // create imap/smtp clients with stubbed tcp sockets
imapClient = new ImapClient({ imapClient = new ImapClient({
auth: { auth: {
@ -231,25 +232,10 @@ describe('Email DAO integration tests', function() {
user: testAccount.user, user: testAccount.user,
xoauth2: testAccount.xoauth2 xoauth2: testAccount.xoauth2
}, },
secure: true, secure: true
ca: ['random string'],
onError: console.error
}); });
smtpClient._TCPSocket = smtpServer.createTCPSocket(); smtpClient._TCPSocket = smtpServer.createTCPSocket();
// stub the onConnect function to inject the test imap/smtp clients
sinon.stub(accountService, 'onConnect', function(cb) {
accountService._emailDao.onConnect({
imapClient: imapClient,
pgpMailer: new PgpMailer({
tls: {
ca: 'random string'
}
}, accountService._pgpbuilder)
}).then(cb).catch(cb);
});
// clear the local database before each test // clear the local database before each test
var cleanup = new DeviceStorageDAO(new LawnchairDAO()); var cleanup = new DeviceStorageDAO(new LawnchairDAO());
cleanup.init(testAccount.user).then(function() { cleanup.init(testAccount.user).then(function() {
@ -260,12 +246,22 @@ describe('Email DAO integration tests', function() {
userStorage = accountService._accountStore; userStorage = accountService._accountStore;
auth = accountService._auth; auth = accountService._auth;
auth.setCredentials({
emailAddress: testAccount.user,
password: 'asd',
smtp: {}, // host and port don't matter here since we're using
imap: {} // a preconfigured smtpclient with mocked tcp sockets
});
auth.init().then(function() { auth.init().then(function() {
accountService.init({ accountService.init({
emailAddress: testAccount.user emailAddress: testAccount.user
}).then(function() { }).then(function() {
emailDao = accountService._emailDao; emailDao = accountService._emailDao;
// retrieve the pgpbuilder from the emaildao and initialize the pgpmailer with the existing pgpbuilder
pgpMailer = new PgpMailer({}, emailDao._pgpbuilder);
// stub rest request to key server // stub rest request to key server
sinon.stub(emailDao._keychain._publicKeyDao, 'get').returns(resolves(mockKeyPair.publicKey)); sinon.stub(emailDao._keychain._publicKeyDao, 'get').returns(resolves(mockKeyPair.publicKey));
sinon.stub(emailDao._keychain._publicKeyDao, 'getByUserId').returns(resolves(mockKeyPair.publicKey)); sinon.stub(emailDao._keychain._publicKeyDao, 'getByUserId').returns(resolves(mockKeyPair.publicKey));
@ -297,9 +293,7 @@ describe('Email DAO integration tests', function() {
passphrase: testAccount.pass, passphrase: testAccount.pass,
keypair: mockKeyPair keypair: mockKeyPair
}).then(function() { }).then(function() {
accountService.onConnect(function(err) { accountService._emailDao.onConnect(imapClient);
expect(err).to.not.exist;
});
}); });
}); });
}); });
@ -310,7 +304,6 @@ describe('Email DAO integration tests', function() {
afterEach(function(done) { afterEach(function(done) {
openpgp.initWorker.restore(); openpgp.initWorker.restore();
mailreader.startWorker.restore(); mailreader.startWorker.restore();
accountService.onConnect.restore();
imapClient.stopListeningForChanges(function() { imapClient.stopListeningForChanges(function() {
imapClient.logout(function() { imapClient.logout(function() {
@ -701,7 +694,7 @@ describe('Email DAO integration tests', function() {
subject: 'plaintext test', subject: 'plaintext test',
body: 'hello world!' body: 'hello world!'
} }
}).then(function() { }, pgpMailer).then(function() {
expect(smtpServer.onmail.callCount).to.equal(1); expect(smtpServer.onmail.callCount).to.equal(1);
done(); done();
}); });
@ -726,7 +719,7 @@ describe('Email DAO integration tests', function() {
body: 'hello world!', body: 'hello world!',
publicKeysArmored: [mockKeyPair.publicKey.publicKey] publicKeysArmored: [mockKeyPair.publicKey.publicKey]
} }
}).then(function() { }, pgpMailer).then(function() {
expect(smtpServer.onmail.callCount).to.equal(1); expect(smtpServer.onmail.callCount).to.equal(1);
done(); done();
}); });
@ -771,7 +764,41 @@ describe('Email DAO integration tests', function() {
subject: 'plaintext test', subject: 'plaintext test',
body: expectedBody body: expectedBody
} }
}).then(function() {}); }, pgpMailer).then(function() {});
});
it('should send & receive a signed encrypted message', function(done) {
var expectedBody = "asdasdasdasdasdasdasdasdasdasdasdasd asdasdasdasdasdasdasdasdasdasdasdasd";
emailDao.onIncomingMessage = function(messages) {
emailDao.getBody({
folder: inbox,
message: messages[0]
}).then(function(message) {
return emailDao.decryptBody({
message: message
});
}).then(function(message) {
expect(message.encrypted).to.be.true;
expect(message.signed).to.be.true;
expect(message.signaturesValid).to.be.true;
expect(message.attachments.length).to.equal(0);
expect(message.body).to.equal(expectedBody);
done();
});
};
emailDao.sendEncrypted({
smtpclient: smtpClient,
email: {
from: [testAccount.user],
to: [testAccount.user],
subject: 'plaintext test',
body: expectedBody,
publicKeysArmored: [mockKeyPair.publicKey.publicKey]
}
}, pgpMailer).then(function() {});
}); });
}); });
}); });

View File

@ -11,7 +11,7 @@ var Account = require('../../../src/js/email/account'),
Dialog = require('../../../src/js/util/dialog'); Dialog = require('../../../src/js/util/dialog');
describe('Account Service unit test', function() { describe('Account Service unit test', function() {
var account, authStub, outboxStub, emailStub, devicestorageStub, keychainStub, updateHandlerStub, pgpbuilderStub, dialogStub, var account, authStub, outboxStub, emailStub, devicestorageStub, keychainStub, updateHandlerStub, dialogStub,
realname = 'John Doe', realname = 'John Doe',
dummyUser = 'spiderpig@springfield.com'; dummyUser = 'spiderpig@springfield.com';
@ -25,9 +25,8 @@ describe('Account Service unit test', function() {
outboxStub = sinon.createStubInstance(Outbox); outboxStub = sinon.createStubInstance(Outbox);
keychainStub = sinon.createStubInstance(Keychain); keychainStub = sinon.createStubInstance(Keychain);
updateHandlerStub = sinon.createStubInstance(UpdateHandler); updateHandlerStub = sinon.createStubInstance(UpdateHandler);
pgpbuilderStub = {};
dialogStub = sinon.createStubInstance(Dialog); dialogStub = sinon.createStubInstance(Dialog);
account = new Account(appConfig, authStub, devicestorageStub, emailStub, outboxStub, keychainStub, updateHandlerStub, pgpbuilderStub, dialogStub); account = new Account(appConfig, authStub, devicestorageStub, emailStub, outboxStub, keychainStub, updateHandlerStub, dialogStub);
}); });
afterEach(function() {}); afterEach(function() {});
@ -200,47 +199,15 @@ describe('Account Service unit test', function() {
}); });
describe('onConnect', function() { describe('onConnect', function() {
var credentials = {
imap: {},
smtp: {}
};
beforeEach(function() { beforeEach(function() {
emailStub._account = {}; emailStub._account = {};
sinon.stub(account, 'isOnline').returns(true);
});
afterEach(function() {
account.isOnline.restore();
});
it('should fail due to _auth.getCredentials', function(done) {
authStub.getCredentials.returns(rejects(new Error('asdf')));
dialogStub.error = function(err) {
expect(err.message).to.match(/asdf/);
done();
};
account.onConnect();
});
it('should fail due to _auth.getCredentials', function(done) {
authStub.getCredentials.returns(rejects(new Error('asdf')));
account.onConnect(function(err) {
expect(err.message).to.match(/asdf/);
expect(dialogStub.error.called).to.be.false;
done();
});
}); });
it('should work', function(done) { it('should work', function(done) {
authStub.getCredentials.returns(resolves(credentials));
authStub.handleCertificateUpdate.returns(resolves());
emailStub.onConnect.returns(resolves()); emailStub.onConnect.returns(resolves());
account.onConnect(function(err) { account.onConnect(function(err) {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(dialogStub.error.called).to.be.false;
expect(emailStub.onConnect.calledOnce).to.be.true; expect(emailStub.onConnect.calledOnce).to.be.true;
done(); done();
}); });

View File

@ -9,6 +9,8 @@ var mailreader = require('mailreader'),
KeychainDAO = require('../../../src/js/service/keychain'), KeychainDAO = require('../../../src/js/service/keychain'),
PGP = require('../../../src/js/crypto/pgp'), PGP = require('../../../src/js/crypto/pgp'),
DeviceStorageDAO = require('../../../src/js/service/devicestorage'), DeviceStorageDAO = require('../../../src/js/service/devicestorage'),
appConfig = require('../../../src/js/app-config'),
Auth = require('../../../src/js/service/auth'),
Dialog = require('../../../src/js/util/dialog'); Dialog = require('../../../src/js/util/dialog');
@ -20,7 +22,7 @@ describe('Email DAO unit tests', function() {
var dao; var dao;
// mocks // mocks
var keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub, parseStub, dialogStub; var keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub, parseStub, dialogStub, authStub;
// config // config
var emailAddress, passphrase, asymKeySize, account; var emailAddress, passphrase, asymKeySize, account;
@ -118,11 +120,12 @@ describe('Email DAO unit tests', function() {
parseStub = sinon.stub(mailreader, 'parse'); parseStub = sinon.stub(mailreader, 'parse');
devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
dialogStub = sinon.createStubInstance(Dialog); dialogStub = sinon.createStubInstance(Dialog);
authStub = sinon.createStubInstance(Auth);
// //
// setup the SUT // setup the SUT
// //
dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader, dialogStub); dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader, dialogStub, appConfig, authStub);
dao._account = account; dao._account = account;
dao._pgpMailer = pgpMailerStub; dao._pgpMailer = pgpMailerStub;
dao._imapClient = imapClientStub; dao._imapClient = imapClientStub;
@ -1616,11 +1619,23 @@ describe('Email DAO unit tests', function() {
}); });
describe('#sendEncrypted', function() { describe('#sendEncrypted', function() {
var publicKeys = ["PUBLIC KEY"], var credentials,
publicKeys,
dummyMail,
msg;
beforeEach(function() {
credentials = {
smtp: {
host: 'foo.io'
}
};
publicKeys = ["PUBLIC KEY"];
dummyMail = { dummyMail = {
publicKeysArmored: publicKeys publicKeysArmored: publicKeys
}, };
msg = 'wow. such message. much rfc2822.'; msg = 'wow. such message. much rfc2822.';
});
it('should send encrypted and upload to sent', function(done) { it('should send encrypted and upload to sent', function(done) {
imapClientStub.uploadMessage.withArgs({ imapClientStub.uploadMessage.withArgs({
@ -1628,6 +1643,7 @@ describe('Email DAO unit tests', function() {
message: msg message: msg
}).yields(); }).yields();
authStub.getCredentials.returns(resolves(credentials));
pgpMailerStub.send.withArgs({ pgpMailerStub.send.withArgs({
encrypt: true, encrypt: true,
mail: dummyMail, mail: dummyMail,
@ -1637,17 +1653,20 @@ describe('Email DAO unit tests', function() {
dao.sendEncrypted({ dao.sendEncrypted({
email: dummyMail email: dummyMail
}).then(function() { }, pgpMailerStub).then(function() {
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true; expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
expect(dao.ignoreUploadOnSent).to.be.false;
done(); done();
}); });
}); });
it('should send encrypted and not upload to sent', function(done) { it('should send encrypted and not upload to sent', function(done) {
dao.ignoreUploadOnSent = true; credentials.smtp.host = 'smtp.gmail.com';
authStub.getCredentials.returns(resolves(credentials));
pgpMailerStub.send.withArgs({ pgpMailerStub.send.withArgs({
encrypt: true, encrypt: true,
mail: dummyMail, mail: dummyMail,
@ -1657,9 +1676,11 @@ describe('Email DAO unit tests', function() {
dao.sendEncrypted({ dao.sendEncrypted({
email: dummyMail email: dummyMail
}).then(function() { }, pgpMailerStub).then(function() {
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false; expect(imapClientStub.uploadMessage.called).to.be.false;
expect(dao.ignoreUploadOnSent).to.be.true;
done(); done();
}); });
@ -1668,12 +1689,14 @@ describe('Email DAO unit tests', function() {
it('should send encrypted and ignore error on upload', function(done) { it('should send encrypted and ignore error on upload', function(done) {
imapClientStub.uploadMessage.yields(new Error()); imapClientStub.uploadMessage.yields(new Error());
pgpMailerStub.send.yieldsAsync(null, msg); pgpMailerStub.send.yieldsAsync(null, msg);
authStub.getCredentials.returns(resolves(credentials));
dao.sendEncrypted({ dao.sendEncrypted({
email: dummyMail email: dummyMail
}).then(function() { }, pgpMailerStub).then(function() {
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true; expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
expect(authStub.getCredentials.calledOnce).to.be.true;
done(); done();
}); });
@ -1681,12 +1704,14 @@ describe('Email DAO unit tests', function() {
it('should not send when pgpmailer fails', function(done) { it('should not send when pgpmailer fails', function(done) {
pgpMailerStub.send.yieldsAsync({}); pgpMailerStub.send.yieldsAsync({});
authStub.getCredentials.returns(resolves(credentials));
dao.sendEncrypted({ dao.sendEncrypted({
email: dummyMail email: dummyMail
}).catch(function(err) { }, pgpMailerStub).catch(function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false; expect(imapClientStub.uploadMessage.called).to.be.false;
@ -1699,8 +1724,9 @@ describe('Email DAO unit tests', function() {
dao.sendEncrypted({ dao.sendEncrypted({
email: dummyMail email: dummyMail
}).catch(function(err) { }, pgpMailerStub).catch(function(err) {
expect(err.code).to.equal(42); expect(err.code).to.equal(42);
expect(authStub.getCredentials.called).to.be.false;
expect(pgpMailerStub.send.called).to.be.false; expect(pgpMailerStub.send.called).to.be.false;
expect(imapClientStub.uploadMessage.called).to.be.false; expect(imapClientStub.uploadMessage.called).to.be.false;
done(); done();
@ -1710,14 +1736,26 @@ describe('Email DAO unit tests', function() {
}); });
describe('#sendPlaintext', function() { describe('#sendPlaintext', function() {
var dummyMail = {}; var credentials,
var msg = 'wow. such message. much rfc2822.'; dummyMail,
msg;
beforeEach(function() {
credentials = {
smtp: {
host: 'foo.io'
}
};
dummyMail = {};
msg = 'wow. such message. much rfc2822.';
});
it('should send in the plain and upload to sent', function(done) { it('should send in the plain and upload to sent', function(done) {
pgpMailerStub.send.withArgs({ pgpMailerStub.send.withArgs({
smtpclient: undefined, smtpclient: undefined,
mail: dummyMail mail: dummyMail
}).yieldsAsync(null, msg); }).yieldsAsync(null, msg);
authStub.getCredentials.returns(resolves(credentials));
imapClientStub.uploadMessage.withArgs({ imapClientStub.uploadMessage.withArgs({
path: sentFolder.path, path: sentFolder.path,
@ -1726,7 +1764,8 @@ describe('Email DAO unit tests', function() {
dao.sendPlaintext({ dao.sendPlaintext({
email: dummyMail email: dummyMail
}).then(function() { }, pgpMailerStub).then(function() {
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true; expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
done(); done();
@ -1735,15 +1774,18 @@ describe('Email DAO unit tests', function() {
it('should send in the plain and not upload to sent', function(done) { it('should send in the plain and not upload to sent', function(done) {
dao.ignoreUploadOnSent = true; dao.ignoreUploadOnSent = true;
credentials.smtp.host = 'smtp.gmail.com';
pgpMailerStub.send.withArgs({ pgpMailerStub.send.withArgs({
smtpclient: undefined, smtpclient: undefined,
mail: dummyMail mail: dummyMail
}).yieldsAsync(null, msg); }).yieldsAsync(null, msg);
authStub.getCredentials.returns(resolves(credentials));
dao.sendPlaintext({ dao.sendPlaintext({
email: dummyMail email: dummyMail
}).then(function() { }, pgpMailerStub).then(function() {
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false; expect(imapClientStub.uploadMessage.called).to.be.false;
done(); done();
@ -1753,10 +1795,12 @@ describe('Email DAO unit tests', function() {
it('should send and ignore error on upload', function(done) { it('should send and ignore error on upload', function(done) {
imapClientStub.uploadMessage.yields(new Error()); imapClientStub.uploadMessage.yields(new Error());
pgpMailerStub.send.yieldsAsync(null, msg); pgpMailerStub.send.yieldsAsync(null, msg);
authStub.getCredentials.returns(resolves(credentials));
dao.sendEncrypted({ dao.sendPlaintext({
email: dummyMail email: dummyMail
}).then(function() { }, pgpMailerStub).then(function() {
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true; expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
@ -1766,11 +1810,13 @@ describe('Email DAO unit tests', function() {
it('should not send due to error', function(done) { it('should not send due to error', function(done) {
pgpMailerStub.send.yieldsAsync({}); pgpMailerStub.send.yieldsAsync({});
authStub.getCredentials.returns(resolves(credentials));
dao.sendPlaintext({ dao.sendPlaintext({
email: dummyMail email: dummyMail
}).catch(function(err) { }, pgpMailerStub).catch(function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(authStub.getCredentials.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false; expect(imapClientStub.uploadMessage.called).to.be.false;
done(); done();
@ -1782,8 +1828,9 @@ describe('Email DAO unit tests', function() {
dao.sendPlaintext({ dao.sendPlaintext({
email: dummyMail email: dummyMail
}).catch(function(err) { }, pgpMailerStub).catch(function(err) {
expect(err.code).to.equal(42); expect(err.code).to.equal(42);
expect(authStub.getCredentials.called).to.be.false;
expect(pgpMailerStub.send.called).to.be.false; expect(pgpMailerStub.send.called).to.be.false;
expect(imapClientStub.uploadMessage.called).to.be.false; expect(imapClientStub.uploadMessage.called).to.be.false;
done(); done();
@ -1805,12 +1852,15 @@ describe('Email DAO unit tests', function() {
describe('event handlers', function() { describe('event handlers', function() {
describe('#onConnect', function() { describe('#onConnect', function() {
var initFoldersStub; var initFoldersStub, credentials;
beforeEach(function() { beforeEach(function() {
initFoldersStub = sinon.stub(dao, '_initFoldersFromImap'); initFoldersStub = sinon.stub(dao, '_initFoldersFromImap');
delete dao._imapClient; delete dao._imapClient;
delete dao._pgpMailer;
credentials = {
imap: {}
};
}); });
it('should connect', function(done) { it('should connect', function(done) {
@ -1818,16 +1868,13 @@ describe('Email DAO unit tests', function() {
uid: 123, uid: 123,
modseq: '123' modseq: '123'
}]; }];
authStub.getCredentials.returns(resolves(credentials));
imapClientStub.login.yieldsAsync(); imapClientStub.login.yieldsAsync();
imapClientStub.selectMailbox.yields(); imapClientStub.selectMailbox.yields();
imapClientStub.listenForChanges.yields(); imapClientStub.listenForChanges.yields();
initFoldersStub.returns(resolves()); initFoldersStub.returns(resolves());
dao.onConnect({ dao.onConnect(imapClientStub).then(function() {
imapClient: imapClientStub,
pgpMailer: pgpMailerStub
}).then(function() {
expect(dao.ignoreUploadOnSent).to.be.false;
expect(imapClientStub.login.calledOnce).to.be.true; expect(imapClientStub.login.calledOnce).to.be.true;
expect(imapClientStub.selectMailbox.calledOnce).to.be.true; expect(imapClientStub.selectMailbox.calledOnce).to.be.true;
expect(initFoldersStub.calledOnce).to.be.true; expect(initFoldersStub.calledOnce).to.be.true;

View File

@ -177,24 +177,6 @@ describe('Auth unit tests', function() {
}); });
describe('#getOAuthToken', function() { describe('#getOAuthToken', function() {
it('should refresh token with known email address', function(done) {
auth.emailAddress = emailAddress;
auth.oauthToken = 'oldToken';
oauthStub.refreshToken.withArgs({
emailAddress: emailAddress,
oldToken: 'oldToken'
}).returns(resolves(oauthToken));
auth.getOAuthToken().then(function() {
expect(auth.emailAddress).to.equal(emailAddress);
expect(auth.oauthToken).to.equal(oauthToken);
expect(oauthStub.refreshToken.calledOnce).to.be.true;
done();
});
});
it('should fetch token with known email address', function(done) { it('should fetch token with known email address', function(done) {
auth.emailAddress = emailAddress; auth.emailAddress = emailAddress;
oauthStub.getOAuthToken.withArgs(emailAddress).returns(resolves(oauthToken)); oauthStub.getOAuthToken.withArgs(emailAddress).returns(resolves(oauthToken));