diff --git a/package.json b/package.json index a719830..b95ccf6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "crypto-lib": "~0.2.1", "imap-client": "~0.3.7", "mailreader": "~0.3.5", - "pgpmailer": "~0.3.10", + "pgpmailer": "~0.3.11", "pgpbuilder": "~0.3.7", "requirejs": "2.1.14", "axe-logger": "~0.0.2", diff --git a/src/js/app-controller.js b/src/js/app-controller.js index efbeaa6..71d4d80 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -133,7 +133,8 @@ define(function(require) { function initClients(credentials) { var pgpMailer = new PgpMailer(credentials.smtp, self._pgpbuilder); var imapClient = new ImapClient(credentials.imap); - imapClient.onError = onImapError; + imapClient.onError = onConnectionError; + pgpMailer.onError = onConnectionError; // certificate update handling imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect, self.onError); @@ -151,19 +152,19 @@ define(function(require) { }, callback); } - function onImapError(error) { - axe.debug('IMAP connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : '')); + 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('IMAP reconnecting...'); + axe.debug('Reconnecting...'); // re-init client modules on error self.onConnect(function(err) { if (err) { - axe.error('IMAP reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : '')); + axe.error('Reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : '')); return; } - axe.debug('IMAP reconnect attempt complete.'); + axe.debug('Reconnect attempt complete.'); }); }, config.reconnectInterval); } diff --git a/src/js/bo/auth.js b/src/js/bo/auth.js index 4c53a33..94eea07 100644 --- a/src/js/bo/auth.js +++ b/src/js/bo/auth.js @@ -256,8 +256,18 @@ define(function(require) { Auth.prototype.getOAuthToken = function(callback) { var self = this; - // get a fresh oauth token - self._oauth.getOAuthToken(self.emailAddress, function(err, oauthToken) { + if (self.oauthToken) { + // removed cached token and get a new one + self._oauth.refreshToken({ + emailAddress: self.emailAddress, + oldToken: self.oauthToken + }, onToken); + } else { + // get a fresh oauth token + self._oauth.getOAuthToken(self.emailAddress, onToken); + } + + function onToken(err, oauthToken) { if (err) { return callback(err); } @@ -278,7 +288,7 @@ define(function(require) { self.emailAddress = emailAddress; callback(); }); - }); + } }; /** diff --git a/src/js/util/oauth.js b/src/js/util/oauth.js index 6ccda2e..8762347 100644 --- a/src/js/util/oauth.js +++ b/src/js/util/oauth.js @@ -5,12 +5,17 @@ define(function() { this._googleApi = googleApi; }; + /** + * Check if chrome.identity api is supported + * @return {Boolean} If is supported + */ OAuth.prototype.isSupported = function() { return !!(window.chrome && chrome.identity); }; /** * Request an OAuth token from chrome for gmail users + * @param {String} emailAddress The user's email address (optional) */ OAuth.prototype.getOAuthToken = function(emailAddress, callback) { var idOptions = { @@ -19,7 +24,7 @@ define(function() { // check which runtime the app is running under chrome.runtime.getPlatformInfo(function(platformInfo) { - if ((chrome && chrome.runtime && chrome.runtime.lastError) || !platformInfo) { + if (chrome.runtime.lastError || !platformInfo) { callback(new Error('Error getting chrome platform info!')); return; } @@ -31,7 +36,7 @@ define(function() { // get OAuth Token from chrome chrome.identity.getAuthToken(idOptions, function(token) { - if ((chrome && chrome.runtime && chrome.runtime.lastError) || !token) { + if (chrome.runtime.lastError || !token) { callback({ errMsg: 'Error fetching an OAuth token for the user!' }); @@ -43,6 +48,32 @@ define(function() { }); }; + /** + * Remove an old OAuth token and get a new one. + * @param {String} options.oldToken The old token to be removed + * @param {String} options.emailAddress The user's email address (optional) + */ + OAuth.prototype.refreshToken = function(options, callback) { + var self = this; + + if (!options.oldToken) { + callback(new Error('oldToken option not set!')); + return; + } + + // remove cached token + chrome.identity.removeCachedAuthToken({ + token: options.oldToken + }, function() { + // get a new token + self.getOAuthToken(options.emailAddress, callback); + }); + }; + + /** + * Get email address from google api + * @param {String} token The oauth token + */ OAuth.prototype.queryEmailAddress = function(token, callback) { if (!token) { callback({ diff --git a/test/unit/auth-test.js b/test/unit/auth-test.js index 98c57be..8fc4dae 100644 --- a/test/unit/auth-test.js +++ b/test/unit/auth-test.js @@ -146,6 +146,26 @@ define(function(require) { }); 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' + }).yieldsAsync(null, oauthToken); + + auth.getOAuthToken(function(err) { + expect(err).to.not.exist; + 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) { auth.emailAddress = emailAddress; oauthStub.getOAuthToken.withArgs(emailAddress).yieldsAsync(null, oauthToken); diff --git a/test/unit/oauth-test.js b/test/unit/oauth-test.js index 84f8cae..39267d5 100644 --- a/test/unit/oauth-test.js +++ b/test/unit/oauth-test.js @@ -6,7 +6,7 @@ define(function(require) { expect = chai.expect; describe('OAuth unit tests', function() { - var oauth, googleApiStub, identityStub, getPlatformInfoStub, + var oauth, googleApiStub, identityStub, getPlatformInfoStub, removeCachedStub, testEmail = 'test@example.com'; beforeEach(function() { @@ -21,6 +21,11 @@ define(function(require) { } identityStub = sinon.stub(window.chrome.identity, 'getAuthToken'); + if (typeof window.chrome.identity.removeCachedAuthToken !== 'function') { + window.chrome.identity.removeCachedAuthToken = function() {}; + } + removeCachedStub = sinon.stub(window.chrome.identity, 'removeCachedAuthToken'); + window.chrome.runtime = window.chrome.runtime || {}; if (typeof window.chrome.runtime.getPlatformInfo !== 'function') { window.chrome.runtime.getPlatformInfo = function() {}; @@ -31,6 +36,7 @@ define(function(require) { afterEach(function() { identityStub.restore(); getPlatformInfoStub.restore(); + removeCachedStub.restore(); }); describe('isSupported', function() { @@ -39,6 +45,61 @@ define(function(require) { }); }); + describe('refreshToken', function() { + var getOAuthTokenStub; + + beforeEach(function() { + getOAuthTokenStub = sinon.stub(oauth, 'getOAuthToken'); + }); + afterEach(function() { + getOAuthTokenStub.restore(); + }); + + it('should work', function() { + removeCachedStub.withArgs({ + token: 'oldToken' + }).yields(); + + getOAuthTokenStub.withArgs(testEmail).yields(); + + oauth.refreshToken({ + oldToken: 'oldToken', + emailAddress: testEmail + }, function(err) { + expect(err).to.not.exist; + expect(removeCachedStub.calledOnce).to.be.true; + expect(getOAuthTokenStub.calledOnce).to.be.true; + }); + }); + + it('should work without email', function() { + removeCachedStub.withArgs({ + token: 'oldToken' + }).yields(); + + getOAuthTokenStub.withArgs(undefined).yields(); + + oauth.refreshToken({ + oldToken: 'oldToken', + }, function(err) { + expect(err).to.not.exist; + expect(removeCachedStub.calledOnce).to.be.true; + expect(getOAuthTokenStub.calledOnce).to.be.true; + expect(getOAuthTokenStub.calledWith(undefined)).to.be.true; + }); + }); + + it('should fail without all options', function() { + oauth.refreshToken({ + emailAddress: testEmail + }, function(err) { + expect(err).to.exist; + expect(removeCachedStub.called).to.be.false; + expect(getOAuthTokenStub.called).to.be.false; + }); + }); + }); + describe('getOAuthToken', function() { it('should work for empty emailAddress', function(done) { getPlatformInfoStub.yields({