mirror of
https://github.com/moparisthebest/mail
synced 2024-11-26 02:42:17 -05:00
[WO-563] Introduce connection doctor
* Uses predefined error messages from the config * Show error and underlying error in login screen
This commit is contained in:
parent
91528f993b
commit
77e0377fd1
@ -177,7 +177,8 @@ define(function(require) {
|
||||
appVersion: appVersion,
|
||||
outboxMailboxPath: 'OUTBOX',
|
||||
outboxMailboxName: 'Outbox',
|
||||
outboxMailboxType: 'Outbox'
|
||||
outboxMailboxType: 'Outbox',
|
||||
connDocTimeout: 5000,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -207,7 +208,14 @@ define(function(require) {
|
||||
bugReportTitle: 'Report a bug',
|
||||
bugReportSubject: '[Bug] I want to report a bug',
|
||||
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider in an anonymized way from the point where you started the app for the last time. Any information provided by you will be used for the porpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\n',
|
||||
supportAddress: 'mail.support@whiteout.io'
|
||||
supportAddress: 'mail.support@whiteout.io',
|
||||
connDocOffline: 'It appears that you are offline. Please retry when you are online.',
|
||||
connDocTlsWrongCert: 'A connection to {0} was rejected because the TLS certificate is invalid. Please have a look at the FAQ for information on how to fix this error.',
|
||||
connDocHostUnreachable: 'We could not establish a connection to {0}. Please check the server settings!',
|
||||
connDocHostTimeout: 'We could not establish a connection to {0} within {1} ms. Please check the server settings and encryption mode!',
|
||||
connDocAuthRejected: 'Your credentials for {0} were rejected. Please check your username and password!',
|
||||
connDocNoInbox: 'We could not detect an IMAP inbox folder on {0}. Please have a look at the FAQ for information on how to fix this error.',
|
||||
connDocGenericError: 'There was an error connecting to {0}: {1}'
|
||||
};
|
||||
|
||||
return app;
|
||||
|
@ -24,6 +24,7 @@ define(function(require) {
|
||||
PrivateKeyDAO = require('js/dao/privatekey-dao'),
|
||||
InvitationDAO = require('js/dao/invitation-dao'),
|
||||
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
|
||||
ConnectionDoctor = require('js/util/connection-doctor'),
|
||||
UpdateHandler = require('js/util/update/update-handler'),
|
||||
config = appConfig.config,
|
||||
str = appConfig.string;
|
||||
@ -104,6 +105,7 @@ define(function(require) {
|
||||
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
|
||||
self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth);
|
||||
self._adminDao = new AdminDao(new RestDAO(config.adminUrl));
|
||||
self._doctor = new ConnectionDoctor();
|
||||
|
||||
emailDao.onError = self.onError;
|
||||
};
|
||||
|
@ -6,21 +6,22 @@ define(function(require) {
|
||||
var ENCRYPTION_METHOD_TLS = 2;
|
||||
|
||||
var appCtrl = require('js/app-controller'),
|
||||
config = require('js/app-config').config,
|
||||
ImapClient = require('imap-client'),
|
||||
SmtpClient = require('smtpclient');
|
||||
config = require('js/app-config').config;
|
||||
|
||||
var SetCredentialsCtrl = function($scope, $location, $routeParams) {
|
||||
if (!appCtrl._emailDao && !$routeParams.dev) {
|
||||
if (!appCtrl._auth && !$routeParams.dev) {
|
||||
$location.path('/'); // init app
|
||||
return;
|
||||
}
|
||||
|
||||
var auth = appCtrl._auth;
|
||||
var doctor = appCtrl._doctor;
|
||||
|
||||
var provider;
|
||||
//
|
||||
// Presets and Settings
|
||||
//
|
||||
|
||||
provider = $location.search().provider;
|
||||
var provider = $location.search().provider;
|
||||
$scope.hasProviderPreset = !!config[provider];
|
||||
$scope.useOAuth = !!auth.oauthToken;
|
||||
$scope.showDetails = (provider === 'custom');
|
||||
@ -30,12 +31,15 @@ define(function(require) {
|
||||
}
|
||||
|
||||
if ($scope.hasProviderPreset) {
|
||||
// use non-editable smtp and imap presets for provider
|
||||
// use non-editable presets
|
||||
|
||||
// SMTP config
|
||||
$scope.smtpHost = config[provider].smtp.host;
|
||||
$scope.smtpPort = config[provider].smtp.port;
|
||||
$scope.smtpCert = config[provider].smtp.ca;
|
||||
$scope.smtpPinned = config[provider].smtp.pinned;
|
||||
|
||||
// transport encryption method
|
||||
if (config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) {
|
||||
$scope.smtpEncryption = ENCRYPTION_METHOD_TLS;
|
||||
} else if (!config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) {
|
||||
@ -44,11 +48,13 @@ define(function(require) {
|
||||
$scope.smtpEncryption = ENCRYPTION_METHOD_NONE;
|
||||
}
|
||||
|
||||
// IMAP config
|
||||
$scope.imapHost = config[provider].imap.host;
|
||||
$scope.imapPort = config[provider].imap.port;
|
||||
$scope.imapCert = config[provider].imap.ca;
|
||||
$scope.imapPinned = config[provider].imap.pinned;
|
||||
|
||||
// transport encryption method
|
||||
if (config[provider].imap.secure && !config[provider].imap.ignoreTLS) {
|
||||
$scope.imapEncryption = ENCRYPTION_METHOD_TLS;
|
||||
} else if (!config[provider].imap.secure && !config[provider].imap.ignoreTLS) {
|
||||
@ -58,109 +64,19 @@ define(function(require) {
|
||||
}
|
||||
}
|
||||
|
||||
$scope.test = function(imapClient, smtpClient) {
|
||||
var imapEncryption = parseInt($scope.imapEncryption, 10);
|
||||
var smtpEncryption = parseInt($scope.smtpEncryption, 10);
|
||||
$scope.credentialsIncomplete = false;
|
||||
$scope.connectionError = false;
|
||||
$scope.smtpOk = undefined;
|
||||
$scope.imapOk = undefined;
|
||||
|
||||
if (!(($scope.username || $scope.emailAddress) && ($scope.password || $scope.useOAuth))) {
|
||||
$scope.credentialsIncomplete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var imap = imapClient || new ImapClient({
|
||||
host: $scope.imapHost.toLowerCase(),
|
||||
port: $scope.imapPort,
|
||||
secure: imapEncryption === ENCRYPTION_METHOD_TLS,
|
||||
ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE,
|
||||
ca: $scope.imapCert,
|
||||
auth: {
|
||||
user: $scope.username || $scope.emailAddress,
|
||||
pass: $scope.password,
|
||||
xoauth2: auth.oauthToken
|
||||
}
|
||||
});
|
||||
|
||||
imap.onCert = function(pemEncodedCert) {
|
||||
if (!$scope.imapPinned) {
|
||||
$scope.imapCert = pemEncodedCert;
|
||||
}
|
||||
};
|
||||
|
||||
imap.onError = function(err) {
|
||||
$scope.imapOk = !err;
|
||||
$scope.connectionError = err;
|
||||
done();
|
||||
};
|
||||
|
||||
var smtp = smtpClient || new SmtpClient($scope.smtpHost.toLowerCase(), $scope.smtpPort, {
|
||||
useSecureTransport: smtpEncryption === ENCRYPTION_METHOD_TLS,
|
||||
ignoreTLS: smtpEncryption === ENCRYPTION_METHOD_NONE,
|
||||
ca: $scope.smtpCert,
|
||||
auth: {
|
||||
user: $scope.username || $scope.emailAddress,
|
||||
pass: $scope.password,
|
||||
xoauth2: auth.oauthToken
|
||||
}
|
||||
});
|
||||
|
||||
smtp.oncert = function(pemEncodedCert) {
|
||||
if (!$scope.smtpPinned) {
|
||||
$scope.smtpCert = pemEncodedCert;
|
||||
}
|
||||
};
|
||||
|
||||
smtp.onerror = function(err) {
|
||||
$scope.smtpOk = !err;
|
||||
$scope.connectionError = $scope.connectionError || err;
|
||||
done();
|
||||
};
|
||||
|
||||
smtp.onidle = function() {
|
||||
smtp.onerror = function() {}; // don't care about errors after discarding connection
|
||||
$scope.smtpOk = true;
|
||||
smtp.quit();
|
||||
done();
|
||||
};
|
||||
|
||||
$scope.busy = 2;
|
||||
|
||||
// fire away
|
||||
imap.login(function(err) {
|
||||
$scope.connectionError = $scope.connectionError || err;
|
||||
$scope.imapOk = !err;
|
||||
imap.logout(function() {}); // don't care about errors after discarding connection
|
||||
done();
|
||||
});
|
||||
|
||||
smtp.connect();
|
||||
};
|
||||
|
||||
function done() {
|
||||
if ($scope.busy > 0) {
|
||||
$scope.busy--;
|
||||
}
|
||||
|
||||
if ($scope.smtpOk && $scope.imapOk) {
|
||||
login();
|
||||
}
|
||||
|
||||
$scope.$apply();
|
||||
}
|
||||
|
||||
function login() {
|
||||
$scope.test = function() {
|
||||
// parse the <select> dropdown lists
|
||||
var imapEncryption = parseInt($scope.imapEncryption, 10);
|
||||
var smtpEncryption = parseInt($scope.smtpEncryption, 10);
|
||||
|
||||
auth.setCredentials({
|
||||
// build credentials object
|
||||
var credentials = {
|
||||
provider: provider,
|
||||
emailAddress: $scope.emailAddress,
|
||||
username: $scope.username || $scope.emailAddress,
|
||||
realname: $scope.realname,
|
||||
password: $scope.password,
|
||||
xoauth2: auth.oauthToken,
|
||||
imap: {
|
||||
host: $scope.imapHost.toLowerCase(),
|
||||
port: $scope.imapPort,
|
||||
@ -177,13 +93,27 @@ define(function(require) {
|
||||
ca: $scope.smtpCert,
|
||||
pinned: !!$scope.smtpPinned
|
||||
}
|
||||
});
|
||||
redirect();
|
||||
}
|
||||
};
|
||||
|
||||
function redirect() {
|
||||
// use the credentials in the connection doctor
|
||||
doctor.configure(credentials);
|
||||
|
||||
// run connection doctor test suite
|
||||
$scope.busy = true;
|
||||
doctor.check(function(err) {
|
||||
if (err) {
|
||||
// display the error in the settings UI
|
||||
$scope.connectionError = err;
|
||||
} else {
|
||||
// persists the credentials and forwards to /login
|
||||
auth.setCredentials(credentials);
|
||||
$location.path('/login');
|
||||
}
|
||||
|
||||
$scope.busy = false;
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return SetCredentialsCtrl;
|
||||
|
289
src/js/util/connection-doctor.js
Normal file
289
src/js/util/connection-doctor.js
Normal file
@ -0,0 +1,289 @@
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var TCPSocket = require('tcp-socket'),
|
||||
appConfig = require('js/app-config'),
|
||||
cfg = appConfig.config,
|
||||
strings = appConfig.string,
|
||||
ImapClient = require('imap-client'),
|
||||
SmtpClient = require('smtpclient');
|
||||
|
||||
/**
|
||||
* The connection doctor can check your connection. In essence, it reconstructs what happens when
|
||||
* the app goes online in an abbreviated way. You need to configure() the instance with the IMAP/SMTP
|
||||
* credentials before running check()!
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
var ConnectionDoctor = function() {};
|
||||
|
||||
|
||||
//
|
||||
// Error codes
|
||||
//
|
||||
|
||||
var OFFLINE = ConnectionDoctor.OFFLINE = 42;
|
||||
var TLS_WRONG_CERT = ConnectionDoctor.TLS_WRONG_CERT = 43;
|
||||
var HOST_UNREACHABLE = ConnectionDoctor.HOST_UNREACHABLE = 44;
|
||||
var HOST_TIMEOUT = ConnectionDoctor.HOST_TIMEOUT = 45;
|
||||
var AUTH_REJECTED = ConnectionDoctor.AUTH_REJECTED = 46;
|
||||
var NO_INBOX = ConnectionDoctor.NO_INBOX = 47;
|
||||
var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48;
|
||||
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
/**
|
||||
* Configures the connection doctor
|
||||
*
|
||||
* @param {Object} credentials.imap IMAP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean)
|
||||
* @param {Object} credentials.smtp SMTP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean)
|
||||
* @param {String} credentials.username
|
||||
* @param {String} credentials.password
|
||||
*/
|
||||
ConnectionDoctor.prototype.configure = function(credentials) {
|
||||
this.credentials = credentials;
|
||||
|
||||
// internal members
|
||||
this._imap = new ImapClient({
|
||||
host: this.credentials.imap.host,
|
||||
port: this.credentials.imap.port,
|
||||
secure: this.credentials.imap.secure,
|
||||
ignoreTLS: this.credentials.imap.ignoreTLS,
|
||||
ca: this.credentials.imap.ca,
|
||||
auth: {
|
||||
user: this.credentials.username,
|
||||
pass: this.credentials.password,
|
||||
xoauth2: this.credentials.xoauth2
|
||||
}
|
||||
});
|
||||
|
||||
this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, {
|
||||
useSecureTransport: this.credentials.smtp.secure,
|
||||
ignoreTLS: this.credentials.smtp.ignoreTLS,
|
||||
ca: this.credentials.smtp.ca,
|
||||
auth: {
|
||||
user: this.credentials.username,
|
||||
pass: this.credentials.password,
|
||||
xoauth2: this.credentials.xoauth2
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* It conducts the following tests for IMAP and SMTP, respectively:
|
||||
* 1) Check if browser is online
|
||||
* 2) Connect to host:port via TCP/TLS
|
||||
* 3) Login to the server
|
||||
* 4) Perform some basic commands (e.g. list folders)
|
||||
* 5) Exposes error codes
|
||||
*
|
||||
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
|
||||
*/
|
||||
ConnectionDoctor.prototype.check = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if (!self.credentials) {
|
||||
return callback(new Error('You need to configure() the connection doctor first!'));
|
||||
}
|
||||
|
||||
self._checkOnline(function(error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
self._checkReachable(self.credentials.imap, function(error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
self._checkReachable(self.credentials.smtp, function(error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
self._checkImap(function(error) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
self._checkSmtp(callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Internal API
|
||||
//
|
||||
|
||||
/**
|
||||
* Checks if the browser is online
|
||||
*
|
||||
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if browser is offline
|
||||
*/
|
||||
ConnectionDoctor.prototype._checkOnline = function(callback) {
|
||||
if (navigator.onLine) {
|
||||
callback();
|
||||
} else {
|
||||
callback(createError(OFFLINE, strings.connDocOffline));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a host is reachable via TCP
|
||||
*
|
||||
* @param {String} options.host
|
||||
* @param {Number} options.port
|
||||
* @param {Boolean} options.secure
|
||||
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
|
||||
*/
|
||||
ConnectionDoctor.prototype._checkReachable = function(options, callback) {
|
||||
var socket,
|
||||
error, // remember the error message
|
||||
timeout, // remember the timeout object
|
||||
host = options.host + ':' + options.port,
|
||||
hasTimedOut = false; // prevents multiple callbacks
|
||||
|
||||
timeout = setTimeout(function() {
|
||||
hasTimedOut = true;
|
||||
callback(createError(HOST_TIMEOUT, strings.connDocHostTimeout.replace('{0}', host).replace('{1}', cfg.connDocTimeout)));
|
||||
}, cfg.connDocTimeout);
|
||||
|
||||
socket = TCPSocket.open(options.host, options.port, {
|
||||
binaryType: 'arraybuffer',
|
||||
useSecureTransport: options.secure,
|
||||
ca: options.ca
|
||||
});
|
||||
|
||||
socket.ondata = function() {}; // we don't actually care about the data
|
||||
|
||||
socket.oncert = function() {
|
||||
if (options.ca) {
|
||||
// the certificate we already have is outdated
|
||||
error = createError(TLS_WRONG_CERT, strings.connDocTlsWrongCert.replace('{0}', host));
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = function(e) {
|
||||
if (!error) {
|
||||
error = createError(HOST_UNREACHABLE, strings.connDocHostUnreachable.replace('{0}', host), e.data);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onopen = function() {
|
||||
socket.close();
|
||||
};
|
||||
|
||||
socket.onclose = function() {
|
||||
if (!hasTimedOut) {
|
||||
clearTimeout(timeout);
|
||||
callback(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an IMAP server is reachable, accepts the credentials, can list folders and has an inbox and logs out.
|
||||
* Adds the certificate to the IMAP settings if not provided.
|
||||
*
|
||||
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
|
||||
*/
|
||||
ConnectionDoctor.prototype._checkImap = function(callback) {
|
||||
var self = this,
|
||||
loggedIn = false,
|
||||
host = self.credentials.imap.host + ':' + self.credentials.imap.port;
|
||||
|
||||
|
||||
self._imap.onCert = function(pemEncodedCert) {
|
||||
if (!self.credentials.imap.ca) {
|
||||
self.credentials.imap.ca = pemEncodedCert;
|
||||
}
|
||||
};
|
||||
|
||||
// login and logout do not use error objects in the callback, but rather invoke
|
||||
// the global onError handler, so we need to track if login was successful
|
||||
self._imap.onError = function(error) {
|
||||
if (!loggedIn) {
|
||||
callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error));
|
||||
} else {
|
||||
callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
|
||||
}
|
||||
};
|
||||
|
||||
self._imap.login(function() {
|
||||
loggedIn = true;
|
||||
|
||||
self._imap.listWellKnownFolders(function(error, wellKnownFolders) {
|
||||
if (error) {
|
||||
return callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
|
||||
}
|
||||
|
||||
if (wellKnownFolders.Inbox.length === 0) {
|
||||
// the client needs at least an inbox folder to work properly
|
||||
return callback(createError(NO_INBOX, strings.connDocNoInbox.replace('{0}', host)));
|
||||
}
|
||||
|
||||
self._imap.logout(function() {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an SMTP server is reachable and accepts the credentials and logs out.
|
||||
* Adds the certificate to the SMTP settings if not provided.
|
||||
*
|
||||
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
|
||||
*/
|
||||
ConnectionDoctor.prototype._checkSmtp = function(callback) {
|
||||
var self = this,
|
||||
host = self.credentials.smtp.host + ':' + self.credentials.smtp.port,
|
||||
errored = false; // tracks if we need to invoke the callback at onclose or not
|
||||
|
||||
self._smtp.oncert = function(pemEncodedCert) {
|
||||
if (!self.credentials.smtp.ca) {
|
||||
self.credentials.smtp.ca = pemEncodedCert;
|
||||
}
|
||||
};
|
||||
|
||||
self._smtp.onerror = function(error) {
|
||||
if (error) {
|
||||
errored = true;
|
||||
callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error));
|
||||
}
|
||||
};
|
||||
|
||||
self._smtp.onidle = function() {
|
||||
self._smtp.quit();
|
||||
};
|
||||
|
||||
self._smtp.onclose = function() {
|
||||
if (!errored) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
self._smtp.connect();
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Helper Functions
|
||||
//
|
||||
|
||||
function createError(code, message, underlyingError) {
|
||||
var error = new Error(message);
|
||||
error.code = code;
|
||||
error.underlyingError = underlyingError;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
return ConnectionDoctor;
|
||||
});
|
@ -11,7 +11,15 @@
|
||||
<form name="form">
|
||||
|
||||
<div>
|
||||
<label class="input-error-message" ng-show="connectionError">Connection failed. Please check your credentials!</label>
|
||||
<div ng-show="connectionError">
|
||||
<p class="input-error-message">{{connectionError.message}}</p>
|
||||
<div ng-show="connectionError.underlyingError">
|
||||
<a href="#" class="input-error-message" wo-touch='showUnderlyingError = !showUnderlyingError; $event.preventDefault()'>{{showUnderlyingError ? "Hide Details" : "Show Details"}}</a>
|
||||
<p ng-show="showUnderlyingError" class="input-error-message">{{connectionError.underlyingError.message}}</p>
|
||||
</div>
|
||||
<a class="input-error-message" href="https://github.com/whiteout-io/mail-html5/wiki/FAQ#troubleshooting" target="_blank">Find out more in the FAQ.</a>
|
||||
</div>
|
||||
|
||||
<label class="input-error-message" ng-show="form.$invalid || credentialsIncomplete">Please fill out all required fields!</label>
|
||||
<br>
|
||||
<input class="input-text" type="email" required ng-model="emailAddress" placeholder="Email address" focus-me="true" tabindex="1" spellcheck="false"></input>
|
||||
|
390
test/unit/connection-doctor-test.js
Normal file
390
test/unit/connection-doctor-test.js
Normal file
@ -0,0 +1,390 @@
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var ConnectionDoctor = require('js/util/connection-doctor'),
|
||||
TCPSocket = require('tcp-socket'),
|
||||
ImapClient = require('imap-client'),
|
||||
SmtpClient = require('smtpclient'),
|
||||
cfg = require('js/app-config').config,
|
||||
expect = chai.expect;
|
||||
|
||||
describe('Connection Doctor', function() {
|
||||
var doctor;
|
||||
var socketStub, imapStub, smtpStub, credentials;
|
||||
|
||||
beforeEach(function() {
|
||||
//
|
||||
// Stubs
|
||||
//
|
||||
|
||||
// there is no socket shim for for this use case, use dummy object
|
||||
socketStub = {
|
||||
close: function() {
|
||||
this.onclose();
|
||||
}
|
||||
};
|
||||
imapStub = sinon.createStubInstance(ImapClient);
|
||||
smtpStub = sinon.createStubInstance(SmtpClient);
|
||||
|
||||
//
|
||||
// Fixture
|
||||
//
|
||||
credentials = {
|
||||
imap: {
|
||||
host: 'asd',
|
||||
port: 1234,
|
||||
secure: true,
|
||||
ca: 'cert'
|
||||
},
|
||||
smtp: {
|
||||
host: 'qwe',
|
||||
port: 5678,
|
||||
secure: false,
|
||||
ca: 'cert'
|
||||
},
|
||||
username: 'username',
|
||||
password: 'password'
|
||||
};
|
||||
|
||||
sinon.stub(TCPSocket, 'open').returns(socketStub); // convenience constructors suck
|
||||
|
||||
//
|
||||
// Setup SUT
|
||||
//
|
||||
doctor = new ConnectionDoctor();
|
||||
doctor.configure(credentials);
|
||||
doctor._imap = imapStub;
|
||||
doctor._smtp = smtpStub;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
TCPSocket.open.restore();
|
||||
});
|
||||
|
||||
describe('#_checkOnline', function() {
|
||||
it('should check if browser is online', function(done) {
|
||||
doctor._checkOnline(function(error) {
|
||||
if (navigator.onLine) {
|
||||
expect(error).to.not.exist;
|
||||
} else {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.OFFLINE);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_checkReachable', function() {
|
||||
it('should be able to reach the host w/o cert', function(done) {
|
||||
credentials.imap.ca = undefined;
|
||||
|
||||
doctor._checkReachable(credentials.imap, function(error) {
|
||||
expect(error).to.not.exist;
|
||||
expect(TCPSocket.open.calledOnce).to.be.true;
|
||||
expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, {
|
||||
binaryType: 'arraybuffer',
|
||||
useSecureTransport: credentials.imap.secure,
|
||||
ca: credentials.imap.ca
|
||||
})).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
socketStub.oncert();
|
||||
socketStub.onopen();
|
||||
});
|
||||
|
||||
it('should fail w/ wrong cert', function(done) {
|
||||
doctor._checkReachable(credentials.imap, function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.TLS_WRONG_CERT);
|
||||
expect(TCPSocket.open.calledOnce).to.be.true;
|
||||
expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, {
|
||||
binaryType: 'arraybuffer',
|
||||
useSecureTransport: credentials.imap.secure,
|
||||
ca: credentials.imap.ca
|
||||
})).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
socketStub.oncert();
|
||||
socketStub.onerror();
|
||||
socketStub.onclose();
|
||||
});
|
||||
|
||||
it('should fail w/ host unreachable', function(done) {
|
||||
doctor._checkReachable(credentials.imap, function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.HOST_UNREACHABLE);
|
||||
expect(TCPSocket.open.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
socketStub.onerror({
|
||||
data: new Error()
|
||||
});
|
||||
socketStub.onclose();
|
||||
});
|
||||
|
||||
it('should fail w/ timeout', function(done) {
|
||||
var origTimeout = cfg.connDocTimeout; // remember timeout from the config to reset it on done
|
||||
cfg.connDocTimeout = 20; // set to 20ms for the test
|
||||
|
||||
doctor._checkReachable(credentials.imap, function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.HOST_TIMEOUT);
|
||||
expect(TCPSocket.open.calledOnce).to.be.true;
|
||||
cfg.connDocTimeout = origTimeout;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_checkImap', function() {
|
||||
it('should perform IMAP login, list folders, logout', function(done) {
|
||||
imapStub.login.yieldsAsync();
|
||||
imapStub.listWellKnownFolders.yieldsAsync(null, {
|
||||
Inbox: [{}]
|
||||
});
|
||||
imapStub.logout.yieldsAsync();
|
||||
|
||||
doctor._checkImap(function(error) {
|
||||
expect(error).to.not.exist;
|
||||
expect(imapStub.login.calledOnce).to.be.true;
|
||||
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
|
||||
expect(imapStub.logout.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail w/ generic error on logout', function(done) {
|
||||
imapStub.login.yieldsAsync();
|
||||
imapStub.listWellKnownFolders.yieldsAsync(null, {
|
||||
Inbox: [{}]
|
||||
});
|
||||
|
||||
doctor._checkImap(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR);
|
||||
expect(error.underlyingError).to.exist;
|
||||
expect(imapStub.login.calledOnce).to.be.true;
|
||||
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
|
||||
expect(imapStub.logout.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
// this error is thrown while we're waiting for the logout
|
||||
imapStub.onError(new Error());
|
||||
}, 50);
|
||||
});
|
||||
|
||||
it('should fail w/ generic error on inbox missing', function(done) {
|
||||
imapStub.login.yieldsAsync();
|
||||
imapStub.listWellKnownFolders.yieldsAsync(null, {
|
||||
Inbox: []
|
||||
});
|
||||
|
||||
doctor._checkImap(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.NO_INBOX);
|
||||
expect(imapStub.login.calledOnce).to.be.true;
|
||||
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
|
||||
expect(imapStub.logout.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail w/ generic error on listing folders fails', function(done) {
|
||||
imapStub.login.yieldsAsync();
|
||||
imapStub.listWellKnownFolders.yieldsAsync(new Error());
|
||||
|
||||
doctor._checkImap(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR);
|
||||
expect(error.underlyingError).to.exist;
|
||||
expect(imapStub.login.calledOnce).to.be.true;
|
||||
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
|
||||
expect(imapStub.logout.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail w/ auth rejected', function(done) {
|
||||
doctor._checkImap(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED);
|
||||
expect(error.underlyingError).to.exist;
|
||||
expect(imapStub.login.calledOnce).to.be.true;
|
||||
expect(imapStub.listWellKnownFolders.called).to.be.false;
|
||||
expect(imapStub.logout.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
// this error is thrown while we're waiting for the login
|
||||
imapStub.onError(new Error());
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_checkSmtp', function() {
|
||||
it('should perform SMTP login, logout', function(done) {
|
||||
doctor._checkSmtp(function(error) {
|
||||
expect(error).to.not.exist;
|
||||
expect(smtpStub.connect.calledOnce).to.be.true;
|
||||
expect(smtpStub.quit.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
smtpStub.onidle();
|
||||
smtpStub.onclose();
|
||||
});
|
||||
|
||||
it('should fail w/ auth rejected', function(done) {
|
||||
doctor._checkSmtp(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED);
|
||||
expect(error.underlyingError).to.exist;
|
||||
expect(smtpStub.connect.calledOnce).to.be.true;
|
||||
expect(smtpStub.quit.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
smtpStub.onerror(new Error());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#check', function() {
|
||||
beforeEach(function() {
|
||||
sinon.stub(doctor, '_checkOnline');
|
||||
sinon.stub(doctor, '_checkReachable');
|
||||
sinon.stub(doctor, '_checkImap');
|
||||
sinon.stub(doctor, '_checkSmtp');
|
||||
});
|
||||
|
||||
it('should perform all tests', function(done) {
|
||||
doctor._checkOnline.yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync();
|
||||
doctor._checkImap.yieldsAsync();
|
||||
doctor._checkSmtp.yieldsAsync();
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.not.exist;
|
||||
expect(doctor._checkOnline.calledOnce).to.be.true;
|
||||
expect(doctor._checkReachable.calledTwice).to.be.true;
|
||||
expect(doctor._checkImap.calledOnce).to.be.true;
|
||||
expect(doctor._checkSmtp.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for smtp', function(done) {
|
||||
doctor._checkOnline.yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync();
|
||||
doctor._checkImap.yieldsAsync();
|
||||
doctor._checkSmtp.yieldsAsync(new Error());
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(doctor._checkOnline.calledOnce).to.be.true;
|
||||
expect(doctor._checkReachable.calledTwice).to.be.true;
|
||||
expect(doctor._checkImap.calledOnce).to.be.true;
|
||||
expect(doctor._checkSmtp.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for imap', function(done) {
|
||||
doctor._checkOnline.yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync();
|
||||
doctor._checkImap.yieldsAsync(new Error());
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(doctor._checkOnline.calledOnce).to.be.true;
|
||||
expect(doctor._checkReachable.calledTwice).to.be.true;
|
||||
expect(doctor._checkImap.calledOnce).to.be.true;
|
||||
expect(doctor._checkSmtp.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for smtp reachability', function(done) {
|
||||
doctor._checkOnline.yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(new Error());
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(doctor._checkOnline.calledOnce).to.be.true;
|
||||
expect(doctor._checkReachable.calledTwice).to.be.true;
|
||||
expect(doctor._checkImap.called).to.be.false;
|
||||
expect(doctor._checkSmtp.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for imap reachability', function(done) {
|
||||
doctor._checkOnline.yieldsAsync();
|
||||
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(new Error());
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(doctor._checkOnline.calledOnce).to.be.true;
|
||||
expect(doctor._checkReachable.calledOnce).to.be.true;
|
||||
expect(doctor._checkImap.called).to.be.false;
|
||||
expect(doctor._checkSmtp.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for offline', function(done) {
|
||||
doctor._checkOnline.yieldsAsync(new Error());
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(doctor._checkOnline.calledOnce).to.be.true;
|
||||
expect(doctor._checkReachable.called).to.be.false;
|
||||
expect(doctor._checkImap.called).to.be.false;
|
||||
expect(doctor._checkSmtp.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail w/o config', function(done) {
|
||||
doctor.credentials = doctor._imap = doctor._smtp = undefined;
|
||||
|
||||
doctor.check(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(doctor._checkOnline.called).to.be.false;
|
||||
expect(doctor._checkReachable.called).to.be.false;
|
||||
expect(doctor._checkImap.called).to.be.false;
|
||||
expect(doctor._checkSmtp.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -4,24 +4,29 @@ define(function(require) {
|
||||
var expect = chai.expect,
|
||||
angular = require('angular'),
|
||||
mocks = require('angularMocks'),
|
||||
ImapClient = require('imap-client'),
|
||||
SmtpClient = require('smtpclient'),
|
||||
Auth = require('js/bo/auth'),
|
||||
ConnectionDoctor = require('js/util/connection-doctor'),
|
||||
SetCredentialsCtrl = require('js/controller/login-set-credentials'),
|
||||
appController = require('js/app-controller');
|
||||
|
||||
describe('Login (Set Credentials) Controller unit test', function() {
|
||||
var scope, location, setCredentialsCtrl;
|
||||
var imap, smtp;
|
||||
var origAuth;
|
||||
var provider = 'providerproviderprovider';
|
||||
// Angular parameters
|
||||
var scope, location, provider;
|
||||
|
||||
// Stubs
|
||||
var auth, origAuth, doctor, origDoctor;
|
||||
|
||||
// SUT
|
||||
var setCredentialsCtrl;
|
||||
|
||||
beforeEach(function() {
|
||||
// remeber pre-test state to restore later
|
||||
origAuth = appController._auth;
|
||||
appController._auth = {};
|
||||
|
||||
imap = sinon.createStubInstance(ImapClient);
|
||||
smtp = sinon.createStubInstance(SmtpClient);
|
||||
origDoctor = appController._doctor;
|
||||
auth = appController._auth = sinon.createStubInstance(Auth);
|
||||
doctor = appController._doctor = sinon.createStubInstance(ConnectionDoctor);
|
||||
|
||||
// setup the controller
|
||||
angular.module('setcredentialstest', []);
|
||||
mocks.module('setcredentialstest');
|
||||
mocks.inject(function($rootScope, $controller, $location) {
|
||||
@ -40,14 +45,13 @@ define(function(require) {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
// restore pre-test state
|
||||
appController._auth = origAuth;
|
||||
appController._doctor = origDoctor;
|
||||
});
|
||||
|
||||
describe('set credentials', function() {
|
||||
it('should work', function(done) {
|
||||
var imapCert = 'imapcertimapcertimapcertimapcertimapcertimapcert',
|
||||
smtpCert = 'smtpcertsmtpcertsmtpcertsmtpcertsmtpcertsmtpcert';
|
||||
|
||||
it('should work', function() {
|
||||
scope.emailAddress = 'emailemailemailemail';
|
||||
scope.password = 'passwdpasswdpasswdpasswd';
|
||||
scope.smtpHost = 'hosthosthost';
|
||||
@ -58,24 +62,19 @@ define(function(require) {
|
||||
scope.imapEncryption = '2'; // TLS
|
||||
scope.realname = 'peter pan';
|
||||
|
||||
imap.login.yields();
|
||||
|
||||
appController._auth.setCredentials = function(args) {
|
||||
expect(smtp.connect.calledOnce).to.be.true;
|
||||
expect(imap.login.calledOnce).to.be.true;
|
||||
|
||||
expect(args).to.deep.equal({
|
||||
var expectedCredentials = {
|
||||
provider: provider,
|
||||
emailAddress: scope.emailAddress,
|
||||
username: scope.username || scope.emailAddress,
|
||||
realname: scope.realname,
|
||||
password: scope.password,
|
||||
xoauth2: undefined,
|
||||
imap: {
|
||||
host: scope.imapHost.toLowerCase(),
|
||||
port: scope.imapPort,
|
||||
secure: true,
|
||||
ignoreTLS: false,
|
||||
ca: scope.imapCert,
|
||||
ca: undefined,
|
||||
pinned: false
|
||||
},
|
||||
smtp: {
|
||||
@ -83,19 +82,19 @@ define(function(require) {
|
||||
port: scope.smtpPort,
|
||||
secure: false,
|
||||
ignoreTLS: false,
|
||||
ca: scope.smtpCert,
|
||||
ca: undefined,
|
||||
pinned: false
|
||||
}
|
||||
});
|
||||
done();
|
||||
};
|
||||
|
||||
scope.test(imap, smtp);
|
||||
doctor.check.yields(); // synchronous yields!
|
||||
|
||||
imap.onCert(imapCert);
|
||||
smtp.oncert(smtpCert);
|
||||
scope.test();
|
||||
|
||||
smtp.onidle();
|
||||
expect(doctor.check.calledOnce).to.be.true;
|
||||
expect(doctor.configure.calledOnce).to.be.true;
|
||||
expect(doctor.configure.calledWith(expectedCredentials)).to.be.true;
|
||||
expect(auth.setCredentials.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -103,7 +103,8 @@ function startTests() {
|
||||
'test/unit/write-ctrl-test',
|
||||
'test/unit/outbox-bo-test',
|
||||
'test/unit/invitation-dao-test',
|
||||
'test/unit/update-handler-test'
|
||||
'test/unit/update-handler-test',
|
||||
'test/unit/connection-doctor-test'
|
||||
], function() {
|
||||
//Tests loaded, run tests
|
||||
mocha.run();
|
||||
|
Loading…
Reference in New Issue
Block a user