Refactor login controllers and delete AppController

This commit is contained in:
Tankred Hase 2014-11-19 20:54:59 +01:00
parent e6b22bd0a0
commit 4c04ba4e74
30 changed files with 455 additions and 571 deletions

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html ng-app="mail" ng-csp manifest="appcache.manifest">
<html ng-csp manifest="appcache.manifest">
<head>
<meta charset="utf-8">
<title>Whiteout Mail</title>

View File

@ -2,7 +2,7 @@
var appCfg = {};
var ngModule = angular.module('mail');
var ngModule = angular.module('woAppConfig');
ngModule.factory('appConfig', function() {
return appCfg;
});

View File

@ -1,319 +0,0 @@
/**
* The main application controller
*/
'use strict';
var axe = require('axe-logger'),
Auth = require('./bo/auth'),
PGP = require('./crypto/pgp'),
OAuth = require('./util/oauth'),
PgpMailer = require('pgpmailer'),
util = require('crypto-lib').util,
PgpBuilder = require('pgpbuilder'),
OutboxBO = require('./bo/outbox'),
mailreader = require('mailreader'),
ImapClient = require('imap-client'),
Crypto = require('./crypto/crypto'),
RestDAO = require('./dao/rest-dao'),
appConfig = require('./app-config'),
EmailDAO = require('./dao/email-dao'),
AdminDao = require('./dao/admin-dao'),
KeychainDAO = require('./dao/keychain-dao'),
PublicKeyDAO = require('./dao/publickey-dao'),
LawnchairDAO = require('./dao/lawnchair-dao'),
PrivateKeyDAO = require('./dao/privatekey-dao'),
InvitationDAO = require('./dao/invitation-dao'),
DeviceStorageDAO = require('./dao/devicestorage-dao'),
ConnectionDoctor = require('./util/connection-doctor'),
UpdateHandler = require('./util/update/update-handler'),
config = appConfig.config,
str = appConfig.string;
var ctrl = {};
/**
* Start the application.
*/
ctrl.start = function(options, callback) {
if (ctrl.started) {
return callback();
}
ctrl.started = true;
ctrl.onError = options.onError; // TODO: replace by errorService
// are we running in a cordova app or in a browser environment?
if (window.cordova) {
// wait for 'deviceready' event to make sure plugins are loaded
axe.debug('Assuming Cordova environment...');
document.addEventListener("deviceready", onDeviceReady, false);
} else {
// No need to wait on events... just start the app
axe.debug('Assuming Browser environment...');
onDeviceReady();
}
function onDeviceReady() {
axe.debug('Starting app.');
// TODO: will be replaced by angular dependency management
ctrl.buildModules();
// TODO: move self-contained connection management in emailDao
// Handle offline and online gracefully
window.addEventListener('online', ctrl.onConnect.bind(ctrl, ctrl.onError));
window.addEventListener('offline', ctrl.onDisconnect.bind(ctrl));
// TODO: move appConfigService shared singleton
ctrl._appConfigStore.init('app-config', callback);
}
};
// TODO: will be replaced by angular dependency management
/**
* Initialize the dependency tree.
*/
ctrl.buildModules = function() {
var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth;
// init objects and inject dependencies
restDao = new RestDAO();
lawnchairDao = new LawnchairDAO();
pubkeyDao = new PublicKeyDAO(restDao);
privkeyDao = new PrivateKeyDAO(new RestDAO(config.privkeyServerUrl));
oauth = new OAuth(new RestDAO('https://www.googleapis.com'));
crypto = new Crypto();
ctrl._pgp = pgp = new PGP();
ctrl._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp);
// TODO: inject dialog service directly into keychain service
keychain.requestPermissionForKeyUpdate = function(params, callback) {
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId);
ctrl.onError({
title: str.updatePublicKeyTitle,
message: message,
positiveBtnStr: str.updatePublicKeyPosBtn,
negativeBtnStr: str.updatePublicKeyNegBtn,
showNegativeBtn: true,
callback: callback
});
};
ctrl._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO());
ctrl._auth = auth = new Auth(appConfigStore, oauth, pgp);
ctrl._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao);
ctrl._invitationDao = new InvitationDAO(restDao);
ctrl._pgpbuilder = pgpbuilder = new PgpBuilder();
ctrl._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);
ctrl._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
ctrl._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth);
ctrl._adminDao = new AdminDao(new RestDAO(config.adminUrl));
ctrl._doctor = new ConnectionDoctor();
emailDao.onError = ctrl.onError;
};
/**
* Calls runtime hooks to check if an app update is available.
*/
ctrl.checkForUpdate = function() {
ctrl._updateHandler.checkForUpdate(ctrl.onError);
};
// TODO: move to AccountService
/**
* Fire up the database, retrieve the available keys for the user and initialize the email data access object
*/
ctrl.init = function(options, callback) {
// account information for the email dao
var account = {
realname: options.realname,
emailAddress: options.emailAddress,
asymKeySize: config.asymKeySize
};
// Pre-Flight check: don't even start to initialize stuff if the email address is not valid
if (!util.validateEmailAddress(options.emailAddress)) {
return callback(new Error('The user email address is invalid!'));
}
prepareDatabase();
// Pre-Flight check: initialize and prepare user's local database
function prepareDatabase() {
ctrl._userStorage.init(options.emailAddress, function(err) {
if (err) {
return callback(err);
}
// Migrate the databases if necessary
ctrl._updateHandler.update(function(err) {
if (err) {
return callback(new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message));
}
prepareKeys();
});
});
}
// retrieve keypair fom devicestorage/cloud, refresh public key if signup was incomplete before
function prepareKeys() {
ctrl._keychain.getUserKeyPair(options.emailAddress, function(err, keys) {
if (err) {
return callback(err);
}
// this is either a first start on a new device, OR a subsequent start without completing the signup,
// since we can't differenciate those cases here, do a public key refresh because it might be outdated
if (keys && keys.publicKey && !keys.privateKey) {
ctrl._keychain.refreshKeyForUserId({
userId: options.emailAddress,
overridePermission: true
}, function(err, publicKey) {
if (err) {
return callback(err);
}
initEmailDao({
publicKey: publicKey
});
});
return;
}
// either signup was complete or no pubkey is available, so we're good here.
initEmailDao(keys);
});
}
function initEmailDao(keys) {
ctrl._emailDao.init({
account: account
}, function(err) {
if (err) {
return callback(err);
}
callback(null, keys);
});
}
};
/**
* Check if the user agent is online.
*/
ctrl.isOnline = function() {
return navigator.onLine;
};
// TODO: move to AccountService
/**
* Event handler that is called when the user agent goes offline.
*/
ctrl.onDisconnect = function() {
ctrl._emailDao.onDisconnect();
};
// TODO: move to AccountService
/**
* Log the current user out by clear the app config store and deleting instances of imap-client and pgp-mailer.
*/
ctrl.logout = function() {
// clear app config store
ctrl._auth.logout(function(err) {
if (err) {
ctrl.onError(err);
return;
}
// delete instance of imap-client and pgp-mailer
ctrl._emailDao.onDisconnect(function(err) {
if (err) {
ctrl.onError(err);
return;
}
if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) {
// reload chrome app
chrome.runtime.reload();
} else {
// navigate to login
window.location.href = '/';
}
});
});
};
// TODO: move onConnect to Account service
/**
* 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.
*/
ctrl.onConnect = function(callback) {
if (!ctrl.isOnline() || !ctrl._emailDao || !ctrl._emailDao._account) {
// prevent connection infinite loop
callback();
return;
}
ctrl._auth.getCredentials(function(err, credentials) {
if (err) {
callback(err);
return;
}
initClients(credentials);
});
function initClients(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, ctrl._pgpbuilder);
var imapClient = new ImapClient(credentials.imap);
imapClient.onError = onConnectionError;
pgpMailer.onError = onConnectionError;
// certificate update handling
imapClient.onCert = ctrl._auth.handleCertificateUpdate.bind(ctrl._auth, 'imap', ctrl.onConnect, ctrl.onError);
pgpMailer.onCert = ctrl._auth.handleCertificateUpdate.bind(ctrl._auth, 'smtp', ctrl.onConnect, ctrl.onError);
// connect to clients
ctrl._emailDao.onConnect({
imapClient: imapClient,
pgpMailer: pgpMailer,
ignoreUploadOnSent: ctrl._emailDao.checkIgnoreUploadOnSent(credentials.imap.host)
}, 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
ctrl.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);
}
};
module.exports = ctrl;

View File

@ -14,7 +14,8 @@ if (typeof window.applicationCache !== 'undefined') {
};
}
var DialogCtrl = require('./controller/dialog'),
var axe = require('axe-logger'),
DialogCtrl = require('./controller/dialog'),
AddAccountCtrl = require('./controller/add-account'),
CreateAccountCtrl = require('./controller/create-account'),
ValidatePhoneCtrl = require('./controller/validate-phone'),
@ -36,13 +37,26 @@ var DialogCtrl = require('./controller/dialog'),
ActionBarCtrl = require('./controller/action-bar'),
errorUtil = require('./util/error'),
backButtonUtil = require('./util/backbutton-handler');
require('./directive/common'),
// include angular modules
require('./app-config');
require('./directive/common');
require('./util');
require('./crypto');
require('./service');
require('./email');
// init main angular module including dependencies
var app = angular.module('mail', [
'ngRoute',
'ngAnimate',
'ngTagsInput',
'woAppConfig',
'woDirectives',
'woUtil',
'woCrypto,',
'woServices',
'woEmail',
'navigation',
'mail-list',
'write',
@ -50,10 +64,7 @@ var app = angular.module('mail', [
'contacts',
'login-new-device',
'privatekey-upload',
'infinite-scroll',
'ngTagsInput',
'woDirectives',
'woServices'
'infinite-scroll'
]);
// set router paths
@ -130,4 +141,25 @@ app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl);
app.controller('ContactsCtrl', ContactsCtrl);
app.controller('AboutCtrl', AboutCtrl);
app.controller('DialogCtrl', DialogCtrl);
app.controller('ActionBarCtrl', ActionBarCtrl);
app.controller('ActionBarCtrl', ActionBarCtrl);
//
// Manual angular bootstraping
//
// are we running in a cordova app or in a browser environment?
if (window.cordova) {
// wait for 'deviceready' event to make sure plugins are loaded
axe.debug('Assuming Cordova environment...');
document.addEventListener('deviceready', bootstrap, false);
} else {
// No need to wait on events... just start the app
axe.debug('Assuming Browser environment...');
bootstrap();
}
function bootstrap() {
angular.element(document).ready(function() {
angular.bootstrap(document, ['mail']);
});
}

View File

@ -5,8 +5,7 @@ var appController = require('../app-controller'),
backBtnHandler = require('../util/backbutton-handler'),
appCfg = require('../app-config'),
config = appCfg.config,
str = appCfg.string,
emailDao, outboxBo;
str = appCfg.string;
//
// Constants
@ -19,14 +18,8 @@ var NOTIFICATION_SENT_TIMEOUT = 2000;
// Controller
//
var NavigationCtrl = function($scope, $routeParams, $location) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
emailDao = appController._emailDao;
outboxBo = appController._outboxBo;
var NavigationCtrl = function($scope, $routeParams, $location, account, email, outbox) {
!$routeParams.dev && !account.isLoggedIn() && $location.path('/'); // init app
//
// scope functions
@ -51,14 +44,14 @@ var NavigationCtrl = function($scope, $routeParams, $location) {
}
// update the outbox mail count
var outbox = _.findWhere($scope.account.folders, {
var ob = _.findWhere($scope.account.folders, {
type: config.outboxMailboxType
});
outbox.count = count;
ob.count = count;
$scope.$apply();
emailDao.refreshFolder({
folder: outbox
email.refreshFolder({
folder: ob
}, $scope.onError);
};
@ -114,18 +107,19 @@ var NavigationCtrl = function($scope, $routeParams, $location) {
}
// get pointer to account/folder/message tree on root scope
$scope.$root.account = emailDao._account;
$scope.$root.account = email._account;
// TODO: $scope.accounts = account.list();
// set notificatio handler for sent messages
outboxBo.onSent = sentNotification;
outbox.onSent = sentNotification;
// start checking outbox periodically
outboxBo.startChecking($scope.onOutboxUpdate);
outbox.startChecking($scope.onOutboxUpdate);
}
function sentNotification(email) {
function sentNotification(message) {
notification.create({
title: 'Message sent',
message: email.subject,
message: message.subject,
timeout: NOTIFICATION_SENT_TIMEOUT
}, function() {});
}

View File

@ -1,12 +1,7 @@
'use strict';
var appCtrl = require('../app-controller');
var AddAccountCtrl = function($scope, $location, $routeParams, mailConfig) {
if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var AddAccountCtrl = function($scope, $location, $routeParams, mailConfig, auth) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.getAccountSettings = function() {
if ($scope.form.$invalid) {
@ -25,7 +20,7 @@ var AddAccountCtrl = function($scope, $location, $routeParams, mailConfig) {
};
var hostname = config.imap.hostname;
if (appCtrl._auth.useOAuth(hostname)) {
if (auth.useOAuth(hostname)) {
// check for oauth support
$scope.oauthPossible();
} else {
@ -62,7 +57,7 @@ var AddAccountCtrl = function($scope, $location, $routeParams, mailConfig) {
function getOAuthToken() {
// fetches the email address from the chrome identity api
appCtrl._auth.getOAuthToken(function(err) {
auth.getOAuthToken(function(err) {
if (err) {
return $scope.onError(err);
}

View File

@ -1,13 +1,7 @@
'use strict';
var appCtrl = require('../app-controller'),
cfg = require('../app-config').config;
var CreateAccountCtrl = function($scope, $location, $routeParams) {
if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var CreateAccountCtrl = function($scope, $location, $routeParams, auth, admin, appConfig) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.createWhiteoutAccount = function() {
if ($scope.form.$invalid) {
@ -17,17 +11,17 @@ var CreateAccountCtrl = function($scope, $location, $routeParams) {
$scope.busy = true;
$scope.errMsg = undefined; // reset error msg
var emailAddress = $scope.user + '@' + cfg.wmailDomain;
var emailAddress = $scope.user + '@' + appConfig.config.wmailDomain;
// set to state for next view
$scope.state.createAccount = {
auth.setCredentials({
emailAddress: emailAddress,
pass: $scope.pass,
password: $scope.pass,
realname: $scope.realname
};
});
// call REST api
appCtrl._adminDao.createUser({
admin.createUser({
emailAddress: emailAddress,
password: $scope.pass,
phone: $scope.phone.replace(/\s+/g, ''), // remove spaces from the phone number

View File

@ -1,14 +1,7 @@
'use strict';
var appController = require('../app-controller');
var LoginExistingCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var emailDao = appController._emailDao;
var LoginExistingCtrl = function($scope, $location, $routeParams, email, auth, keychain) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.confirmPassphrase = function() {
if ($scope.form.$invalid) {
@ -24,14 +17,14 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
};
function unlockCrypto() {
var userId = emailDao._account.emailAddress;
emailDao._keychain.getUserKeyPair(userId, function(err, keypair) {
var userId = auth.emailAddress;
keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) {
displayError(err);
return;
}
emailDao.unlock({
email.unlock({
keypair: keypair,
passphrase: $scope.passphrase
}, onUnlock);
@ -44,7 +37,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
return;
}
appController._auth.storeCredentials(function(err) {
auth.storeCredentials(function(err) {
if (err) {
displayError(err);
return;

View File

@ -1,17 +1,9 @@
'use strict';
var appController = require('../app-controller');
var LoginInitialCtrl = function($scope, $location, $routeParams, newsletter, email, auth) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
var LoginInitialCtrl = function($scope, $location, $routeParams, newsletter) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
if (appController._emailDao) {
var emailDao = appController._emailDao,
emailAddress = emailDao._account.emailAddress;
}
var emailAddress = auth.emailAddress;
var termsMsg = 'You must accept the Terms of Service to continue.',
states = {
@ -59,7 +51,7 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, newsletter) {
// go to set keygen screen
$scope.setState(states.PROCESSING);
emailDao.unlock({
email.unlock({
passphrase: undefined // generate key without passphrase
}, function(err) {
if (err) {
@ -67,7 +59,7 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, newsletter) {
return;
}
appController._auth.storeCredentials(function(err) {
auth.storeCredentials(function(err) {
if (err) {
displayError(err);
return;

View File

@ -1,15 +1,7 @@
'use strict';
var appController = require('../app-controller');
var LoginExistingCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var emailDao = appController._emailDao,
pgp = appController._pgp;
var LoginExistingCtrl = function($scope, $location, $routeParams, email, auth, pgp, keychain) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.incorrect = false;
@ -27,9 +19,9 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
};
function unlockCrypto() {
var userId = emailDao._account.emailAddress;
var userId = auth.emailAddress;
// check if user already has a public key on the key server
emailDao._keychain.getUserKeyPair(userId, function(err, keypair) {
keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) {
$scope.displayError(err);
return;
@ -76,7 +68,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
}
// import and validate keypair
emailDao.unlock({
email.unlock({
keypair: keypair,
passphrase: $scope.passphrase
}, function(err) {
@ -86,7 +78,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
return;
}
emailDao._keychain.putUserKeyPair(keypair, onUnlock);
keychain.putUserKeyPair(keypair, onUnlock);
});
});
}
@ -97,7 +89,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
return;
}
appController._auth.storeCredentials(function(err) {
auth.storeCredentials(function(err) {
if (err) {
$scope.displayError(err);
return;

View File

@ -1,18 +1,7 @@
'use strict';
var appController = require('../app-controller');
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
if (appController._emailDao) {
var keychain = appController._keychain,
emailDao = appController._emailDao,
userId = emailDao._account.emailAddress;
}
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, auth, email, keychain) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.step = 1;
@ -38,6 +27,7 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
};
$scope.verifyRecoveryToken = function(callback) {
var userId = auth.emailAddress;
keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) {
displayError(err);
@ -95,7 +85,7 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase
emailDao.unlock({
email.unlock({
keypair: $scope.cachedKeypair,
passphrase: undefined
}, function(err) {
@ -106,7 +96,7 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
}
// passphrase is corrent ... go to main app
appController._auth.storeCredentials(function(err) {
auth.storeCredentials(function(err) {
if (err) {
displayError(err);
return;

View File

@ -4,16 +4,8 @@ var ENCRYPTION_METHOD_NONE = 0;
var ENCRYPTION_METHOD_STARTTLS = 1;
var ENCRYPTION_METHOD_TLS = 2;
var appCtrl = require('../app-controller');
var SetCredentialsCtrl = function($scope, $location, $routeParams) {
if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var auth = appCtrl._auth;
var doctor = appCtrl._doctor;
var SetCredentialsCtrl = function($scope, $location, $routeParams, auth, connectionDoctor) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
//
// Presets and Settings
@ -87,11 +79,11 @@ var SetCredentialsCtrl = function($scope, $location, $routeParams) {
};
// use the credentials in the connection doctor
doctor.configure(credentials);
connectionDoctor.configure(credentials);
// run connection doctor test suite
$scope.busy = true;
doctor.check(function(err) {
connectionDoctor.check(function(err) {
if (err) {
// display the error in the settings UI
$scope.connectionError = err;

View File

@ -1,31 +1,19 @@
'use strict';
var appController = require('../app-controller');
var LoginCtrl = function($scope, $location, updateHandler, account, auth, email, keychain, dialog) {
var LoginCtrl = function($scope, $location) {
// start main application controller
appController.start({
onError: $scope.onError
}, function(err) {
if (err) {
$scope.onError(err);
return;
}
// check for app update
appController.checkForUpdate();
initializeUser();
});
// TODO: move to Account service login function
// check for app update
updateHandler.checkForUpdate();
// initialize the user account
initializeUser();
function initializeUser() {
// init the auth modules
auth.init();
// get OAuth token from chrome
appController._auth.getEmailAddress(function(err, info) {
auth.getEmailAddress(function(err, info) {
if (err) {
$scope.onError(err);
dialog.error(err);
return;
}
@ -35,13 +23,13 @@ var LoginCtrl = function($scope, $location) {
return;
}
// initiate controller by creating email dao
appController.init({
// initiate the account by initializing the email dao and user storage
account.init({
emailAddress: info.emailAddress,
realname: info.realname
}, function(err, availableKeys) {
if (err) {
$scope.onError(err);
dialog.error(err);
return;
}
@ -53,7 +41,7 @@ var LoginCtrl = function($scope, $location) {
function redirect(availableKeys) {
if (availableKeys && availableKeys.publicKey && availableKeys.privateKey) {
// public and private key available, try empty passphrase
appController._emailDao.unlock({
email.unlock({
keypair: availableKeys,
passphrase: undefined
}, function(err) {
@ -62,9 +50,9 @@ var LoginCtrl = function($scope, $location) {
return;
}
appController._auth.storeCredentials(function(err) {
auth.storeCredentials(function(err) {
if (err) {
return $scope.onError(err);
return dialog.error(err);
}
goTo('/desktop');
@ -72,12 +60,12 @@ var LoginCtrl = function($scope, $location) {
});
} else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) {
// check if private key is synced
appController._keychain.requestPrivateKeyDownload({
keychain.requestPrivateKeyDownload({
userId: availableKeys.publicKey.userId,
keyId: availableKeys.publicKey._id,
}, function(err, privateKeySynced) {
if (err) {
$scope.onError(err);
dialog.error(err);
return;
}

View File

@ -1,12 +1,7 @@
'use strict';
var appCtrl = require('../app-controller');
var ValidatePhoneCtrl = function($scope, $location, $routeParams, mailConfig) {
if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var ValidatePhoneCtrl = function($scope, $location, $routeParams, mailConfig, auth, admin) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
// TODO: move to Account service create function
@ -20,8 +15,8 @@ var ValidatePhoneCtrl = function($scope, $location, $routeParams, mailConfig) {
$scope.errMsg = undefined; // reset error msg
// verify user to REST api
appCtrl._adminDao.validateUser({
emailAddress: $scope.state.createAccount.emailAddress,
admin.validateUser({
emailAddress: auth.emailAddress,
token: $scope.token.toUpperCase()
}, function(err) {
if (err) {
@ -37,14 +32,14 @@ var ValidatePhoneCtrl = function($scope, $location, $routeParams, mailConfig) {
};
$scope.login = function() {
var address = $scope.state.createAccount.emailAddress;
var address = auth.emailAddress;
return mailConfig.get(address).then(function(config) {
// store credentials in memory
appCtrl._auth.setCredentials({
emailAddress: $scope.state.createAccount.emailAddress,
username: $scope.state.createAccount.emailAddress,
realname: $scope.state.createAccount.realname,
password: $scope.state.createAccount.pass,
auth.setCredentials({
emailAddress: auth.emailAddress,
username: auth.emailAddress,
realname: auth.realname,
password: auth.password,
imap: {
host: config.imap.hostname,
port: parseInt(config.imap.port, 10),

View File

@ -1,6 +1,6 @@
'use strict';
angular.module('woCrypto', []);
angular.module('woCrypto', ['woAppConfig', 'woUtil']);
require('./pgp');
require('./crypto');

View File

@ -4,41 +4,240 @@ var ngModule = angular.module('woServices');
ngModule.service('account', Account);
module.exports = Account;
function Account(email, outbox) {
this._emailDAOs = [email];
this._outboxes = [outbox];
this._accounts = undefined;
var axe = require('axe-logger'),
util = require('crypto-lib').util,
PgpMailer = require('pgpmailer'),
ImapClient = require('imap-client');
function Account(appConfig, auth, admin, mailConfig, keychain, pgpbuilder, email, outbox, deviceStorage, updateHandler) {
this._appConfig = appConfig;
this._auth = auth;
this._admin = admin;
this._mailConfig = mailConfig;
this._keychain = keychain;
this._emailDao = email;
this._pgpbuilder = pgpbuilder;
this._outbox = outbox;
this._deviceStorage = deviceStorage;
this._updateHandler = updateHandler;
this._accounts = []; // init accounts list
}
/**
* Check if the account is already logged in.
* @return {Boolean} if the account is logged in
*/
Account.prototype.isLoggedIn = function() {
return (this._accounts.length > 0);
};
/**
* Lists all of the current accounts connected to the app
* @return {Array<Object>} The account objects containing folder and message objects
*/
Account.prototype.list = function() {
this._accounts = this._emailDAOs.map(function(emailDao) {
return emailDao._account;
});
return this._accounts;
};
/**
* Login to an existing email account. This creates a new email data access object instance for that account and logs in via IMAP.
* @param {String} options.emailAddress The account's email address
* Fire up the database, retrieve the available keys for the user and initialize the email data access object
*/
Account.prototype.login = function(options) {
var emailDao = new Email();
this._emailDAOs.push(emailDao);
Account.prototype.init = function(options, callback) {
var self = this;
// account information for the email dao
var account = {
realname: options.realname,
emailAddress: options.emailAddress,
asymKeySize: this._appConfig.asymKeySize
};
// Pre-Flight check: don't even start to initialize stuff if the email address is not valid
if (!util.validateEmailAddress(options.emailAddress)) {
return callback(new Error('The user email address is invalid!'));
}
prepareDatabase();
// Pre-Flight check: initialize and prepare user's local database
function prepareDatabase() {
self._deviceStorage.init(options.emailAddress, function(err) {
if (err) {
return callback(err);
}
// Migrate the databases if necessary
self._updateHandler.update(function(err) {
if (err) {
return callback(new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message));
}
prepareKeys();
});
});
}
// retrieve keypair fom devicestorage/cloud, refresh public key if signup was incomplete before
function prepareKeys() {
self._keychain.getUserKeyPair(options.emailAddress, function(err, keys) {
if (err) {
return callback(err);
}
// this is either a first start on a new device, OR a subsequent start without completing the signup,
// since we can't differenciate those cases here, do a public key refresh because it might be outdated
if (keys && keys.publicKey && !keys.privateKey) {
self._keychain.refreshKeyForUserId({
userId: options.emailAddress,
overridePermission: true
}, function(err, publicKey) {
if (err) {
return callback(err);
}
initEmailDao({
publicKey: publicKey
});
});
return;
}
// either signup was complete or no pubkey is available, so we're good here.
initEmailDao(keys);
});
}
function initEmailDao(keys) {
self._emailDao.init({
account: account
}, function(err) {
if (err) {
return callback(err);
}
// Handle offline and online gracefully ... arm dom event
window.addEventListener('online', self.onConnect.bind(self));
window.addEventListener('offline', self.onDisconnect.bind(self));
// add account object to the accounts array for the ng controllers
self._accounts.push(account);
callback(null, keys);
});
}
};
/**
* 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.
*/
Account.prototype.onConnect = function(callback) {
var self = this;
var config = self._appConfig.config;
if (!self.isOnline() || !self._emailDao || !self._emailDao._account) {
// prevent connection infinite loop
callback();
return;
}
self._auth.getCredentials(function(err, credentials) {
if (err) {
self._dialog.error(err);
return;
}
initClients(credentials);
});
function initClients(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, self._dialog.error);
pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect, self._dialog.error);
// connect to clients
self._emailDao.onConnect({
imapClient: imapClient,
pgpMailer: pgpMailer,
ignoreUploadOnSent: self._emailDao.checkIgnoreUploadOnSent(credentials.imap.host)
}, self._dialog.error);
}
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);
}
};
/**
* Event handler that is called when the user agent goes offline.
*/
Account.prototype.onDisconnect = function() {
this._emailDao.onDisconnect();
};
/**
* Logout of an email account. Log the current user out by clear the app config store and deleting instances of imap-client and pgp-mailer.
*/
Account.prototype.logout = function() {
var self = this;
// clear app config store
self._auth.logout(function(err) {
if (err) {
self._dialog.error(err);
return;
}
// delete instance of imap-client and pgp-mailer
self._emailDao.onDisconnect(function(err) {
if (err) {
self._dialog.error(err);
return;
}
if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) {
// reload chrome app
chrome.runtime.reload();
} else {
// navigate to login
window.location.href = '/';
}
});
});
};
/**
* Create a new whiteout account. This creates a new email data access object instance for that account and logs in via IMAP.
* @param {String} options.emailAddress The account's email address
*/
Account.prototype.create = function(options) {};
/**
* Logout of an email account. This creates a new email data access object instance for that account and logs in via IMAP.
* @param {String} options.emailAddress The account's email address
*/
Account.prototype.logout = function(options) {};
Account.prototype.create = function() {};

View File

@ -50,12 +50,14 @@ var MSG_PART_TYPE_HTML = 'html';
* @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages
* @param {Object} mailreader Parses MIME messages received from IMAP
*/
function Email(keychain, pgp, devicestorage, pgpbuilder, mailreader) {
function Email(keychain, pgp, deviceStorage, pgpbuilder, mailreader, dialog) {
this._keychain = keychain;
this._pgp = pgp;
this._devicestorage = devicestorage;
this._devicestorage = deviceStorage;
this._pgpbuilder = pgpbuilder;
this._mailreader = mailreader;
this.onError = dialog.error;
}

View File

@ -1,6 +1,9 @@
'use strict';
angular.module('woEmail', []);
angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices']);
require('./mailreader');
require('./pgpbuilder');
require('./email');
require('./outbox');
require('./account');

View File

@ -13,15 +13,15 @@ var util = require('crypto-lib').util,
* The local outbox takes care of the emails before they are being sent.
* It also checks periodically if there are any mails in the local device storage to be sent.
*/
function Outbox(emailDao, keychain, devicestorage) {
function Outbox(email, keychain, deviceStorage) {
/** @private */
this._emailDao = emailDao;
this._emailDao = email;
/** @private */
this._keychain = keychain;
/** @private */
this._devicestorage = devicestorage;
this._devicestorage = deviceStorage;
/**
* Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process.

View File

@ -4,8 +4,9 @@ var ngModule = angular.module('woServices');
ngModule.service('admin', Admin);
module.exports = Admin;
function Admin(restDao) {
function Admin(restDao, appConfig) {
this._restDao = restDao;
this._restDao.setBaseUri(appConfig.config.adminUrl);
}
/**

View File

@ -0,0 +1,15 @@
'use strict';
var ngModule = angular.module('woServices');
ngModule.service('appConfigStore', AppConfigStore);
module.exports = AppConfigStore;
/**
* A service for storing app configuration and user credential data locally
*/
function AppConfigStore(lawnchairDAO) {
this._localDbDao = lawnchairDAO;
this._localDbDao.init('app-config');
}
// TODO: inherit DeviceStorage service api

View File

@ -25,12 +25,26 @@ var SMTP_DB_KEY = 'smtp';
* auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP,
* username, password / oauth token, IMAP/SMTP server host names, ...
*/
function Auth(deviceStorage, oauth, pgp) {
this._appConfigStore = deviceStorage;
function Auth(appConfigStore, oauth, pgp) {
this._appConfigStore = appConfigStore;
this._oauth = oauth;
this._pgp = pgp;
}
/**
* Initialize the service
*/
Auth.prototype.init = function() {
this._initialized = true;
};
/**
* Check if the service has been initialized.
*/
Auth.prototype.isInitialized = function() {
return this._initialized;
};
/**
* Retrieves credentials and IMAP/SMTP settings:
* 1) Fetches the credentials from disk, then...

View File

@ -1,22 +1,18 @@
'use strict';
var ngModule = angular.module('woServices');
ngModule.factory('deviceStorage', ['lawnchairDAO',
function(lawnchairDAO) {
return new DeviceStorage(lawnchairDAO);
}
]);
ngModule.service('deviceStorage', DeviceStorage);
module.exports = DeviceStorage;
/**
* High level storage api that handles all persistence on the device.
* High level storage api that handles all persistence of a user's data on the device.
*/
function DeviceStorage(lawnchairDAO) {
this._localDbDao = lawnchairDAO;
}
DeviceStorage.prototype.init = function(dbName, callback) {
this._localDbDao.init(dbName, callback);
DeviceStorage.prototype.init = function(dbName) {
this._localDbDao.init(dbName);
};
/**

View File

@ -1,8 +1,17 @@
'use strict';
angular.module('woServices', []);
angular.module('woServices', ['woAppConfig', 'woUtil', 'woCrypto']);
require('./newsletter');
require('./rest');
require('./invitation');
require('./mail-config');
require('./account');
require('.pgpbuilder');
require('./newsletter');
require('./oauth');
require('./privatekey');
require('./publickey');
require('./admin');
require('./lawnchair');
require('./devicestorage');
require('./app-config-store');
require('./auth');
require('./keychain');

View File

@ -4,15 +4,13 @@ var ngModule = angular.module('woServices');
ngModule.service('invitation', Invitation);
module.exports = Invitation;
var config = require('../app-config').config;
/**
* The Invitation is a high level Data Access Object that access the invitation service REST endpoint.
* @param {Object} restDao The REST Data Access Object abstraction
*/
function Invitation(restDao) {
function Invitation(restDao, appConfig) {
this._restDao = restDao;
this._restDao.setBaseUri(config.cloudUrl);
this._restDao.setBaseUri(appConfig.config.cloudUrl);
}
//

View File

@ -16,18 +16,40 @@ var DB_PUBLICKEY = 'publickey',
* A high-level Data-Access Api for handling Keypair synchronization
* between the cloud service and the device's local storage
*/
function Keychain(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) {
this._localDbDao = localDbDao;
this._publicKeyDao = publicKeyDao;
this._privateKeyDao = privateKeyDao;
function Keychain(lawnchairDAO, publicKey, privateKey, crypto, pgp, dialog, appConfig) {
this._lawnchairDAO = lawnchairDAO;
this._publicKeyDao = publicKey;
this._privateKeyDao = privateKey;
this._crypto = crypto;
this._pgp = pgp;
this._dialog = dialog;
this._appConfig = appConfig;
}
//
// Public key functions
//
/**
* Display confirmation dialog to request a public key update
* @param {Object} params.newKey The user's updated public key object
* @param {String} params.userId The user's email address
*/
Keychain.prototype.requestPermissionForKeyUpdate = function(params, callback) {
var str = this._appConfig.string;
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId);
this._dialog.confirm({
title: str.updatePublicKeyTitle,
message: message,
positiveBtnStr: str.updatePublicKeyPosBtn,
negativeBtnStr: str.updatePublicKeyNegBtn,
showNegativeBtn: true,
callback: callback
});
};
/**
* Verifies the public key of a user o nthe public key store
* @param {String} uuid The uuid to verify the key
@ -196,7 +218,7 @@ Keychain.prototype.getReceiverPublicKey = function(userId, callback) {
var self = this;
// search local keyring for public key
self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
self._lawnchairDAO.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
if (err) {
callback(err);
return;
@ -275,7 +297,7 @@ Keychain.prototype.setDeviceName = function(deviceName, callback) {
return;
}
this._localDbDao.persist(DB_DEVICENAME, deviceName, callback);
this._lawnchairDAO.persist(DB_DEVICENAME, deviceName, callback);
};
/**
@ -285,7 +307,7 @@ Keychain.prototype.setDeviceName = function(deviceName, callback) {
*/
Keychain.prototype.getDeviceName = function(callback) {
// check if deviceName is already persisted in storage
this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) {
this._lawnchairDAO.read(DB_DEVICENAME, function(err, deviceName) {
if (err) {
callback(err);
return;
@ -308,7 +330,7 @@ Keychain.prototype.getDeviceSecret = function(callback) {
var self = this;
// generate random deviceSecret or get from storage
self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) {
self._lawnchairDAO.read(DB_DEVICE_SECRET, function(err, storedDevSecret) {
if (err) {
callback(err);
return;
@ -323,7 +345,7 @@ Keychain.prototype.getDeviceSecret = function(callback) {
// generate random deviceSecret
var deviceSecret = util.random(config.symKeySize);
// persist deviceSecret to local storage (in plaintext)
self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) {
self._lawnchairDAO.persist(DB_DEVICE_SECRET, deviceSecret, function(err) {
if (err) {
callback(err);
return;
@ -763,7 +785,7 @@ Keychain.prototype.getUserKeyPair = function(userId, callback) {
var self = this;
// search for user's public key locally
self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
self._lawnchairDAO.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
if (err) {
callback(err);
return;
@ -886,7 +908,7 @@ Keychain.prototype.lookupPublicKey = function(id, callback) {
}
// lookup in local storage
self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) {
self._lawnchairDAO.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) {
if (err) {
callback(err);
return;
@ -922,26 +944,26 @@ Keychain.prototype.lookupPublicKey = function(id, callback) {
*/
Keychain.prototype.listLocalPublicKeys = function(callback) {
// search local keyring for public key
this._localDbDao.list(DB_PUBLICKEY, 0, null, callback);
this._lawnchairDAO.list(DB_PUBLICKEY, 0, null, callback);
};
Keychain.prototype.removeLocalPublicKey = function(id, callback) {
this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback);
this._lawnchairDAO.remove(DB_PUBLICKEY + '_' + id, callback);
};
Keychain.prototype.lookupPrivateKey = function(id, callback) {
// lookup in local storage
this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback);
this._lawnchairDAO.read(DB_PRIVATEKEY + '_' + id, callback);
};
Keychain.prototype.saveLocalPublicKey = function(pubkey, callback) {
// persist public key (email, _id)
var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id;
this._localDbDao.persist(pkLookupKey, pubkey, callback);
this._lawnchairDAO.persist(pkLookupKey, pubkey, callback);
};
Keychain.prototype.saveLocalPrivateKey = function(privkey, callback) {
// persist private key (email, _id)
var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id;
this._localDbDao.persist(prkLookupKey, privkey, callback);
this._lawnchairDAO.persist(prkLookupKey, privkey, callback);
};

View File

@ -15,25 +15,13 @@ function LawnchairDAO() {}
* Initialize the lawnchair database
* @param {String} dbName The name of the database
*/
LawnchairDAO.prototype.init = function(dbName, callback) {
LawnchairDAO.prototype.init = function(dbName) {
if (!dbName) {
callback({
errMsg: 'Lawnchair DB name must be specified!'
});
return;
throw new Error('Lawnchair DB name must be specified!');
}
this._db = new Lawnchair({
name: dbName
}, function(lc) {
if (!lc) {
callback({
errMsg: 'Lawnchair init failed!'
});
return;
}
callback();
});
};

View File

@ -4,11 +4,9 @@ var ngModule = angular.module('woServices');
ngModule.service('privateKey', PrivateKey);
module.exports = PrivateKey;
var config = require('../app-config').config;
function PrivateKey(restDao) {
function PrivateKey(restDao, appConfig) {
this._restDao = restDao;
this._restDao.setBaseUri(config.privkeyServerUrl);
this._restDao.setBaseUri(appConfig.config.privkeyServerUrl);
}
//

View File

@ -4,11 +4,9 @@ var ngModule = angular.module('woServices');
ngModule.service('publicKey', PublicKey);
module.exports = PublicKey;
var config = require('../app-config').config;
function PublicKey(restDao) {
function PublicKey(restDao, appConfig) {
this._restDao = restDao;
this._restDao.setBaseUri(config.cloudUrl);
this._restDao.setBaseUri(appConfig.config.cloudUrl);
}
/**

View File

@ -1,7 +1,7 @@
'use strict';
var ngModule = angular.module('woUtil');
ngModule.service('updateHandler', ['deviceStorage', 'deviceStorage', 'auth', UpdateHandler]);
ngModule.service('updateHandler', UpdateHandler);
module.exports = UpdateHandler;
var axe = require('axe-logger'),
@ -15,11 +15,12 @@ var axe = require('axe-logger'),
/**
* Handles database migration
*/
function UpdateHandler(appConfigStorage, userStorage, auth) {
this._appConfigStorage = appConfigStorage;
this._userStorage = userStorage;
function UpdateHandler(appConfigStore, deviceStorage, auth, dialog) {
this._appConfigStorage = appConfigStore;
this._userStorage = deviceStorage;
this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5];
this._auth = auth;
this._dialog = dialog;
}
/**
@ -99,14 +100,16 @@ UpdateHandler.prototype._applyUpdate = function(options, callback) {
/**
* Check application version and update correspondingly
*/
UpdateHandler.prototype.checkForUpdate = function(dialog) {
UpdateHandler.prototype.checkForUpdate = function() {
var self = this;
// Chrome Packaged App
if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.onUpdateAvailable) {
// check for Chrome app update and restart
chrome.runtime.onUpdateAvailable.addListener(function(details) {
axe.debug('New Chrome App update... requesting reload.');
// Chrome downloaded a new app version
dialog({
self._dialog.confirm({
title: 'Update available',
message: 'A new version ' + details.version + ' of the app is available. Restart the app to update?',
positiveBtnStr: 'Restart',