mirror of
https://github.com/moparisthebest/mail
synced 2025-02-24 06:41:53 -05:00
451 lines
14 KiB
JavaScript
451 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
var axe = require('axe-logger'),
|
|
str = require('../app-config').string;
|
|
|
|
var EMAIL_ADDR_DB_KEY = 'emailaddress';
|
|
var USERNAME_DB_KEY = 'username';
|
|
var REALNAME_DB_KEY = 'realname';
|
|
var PASSWD_DB_KEY = 'password';
|
|
var PROVIDER_DB_KEY = 'provider';
|
|
var IMAP_DB_KEY = 'imap';
|
|
var SMTP_DB_KEY = 'smtp';
|
|
|
|
/**
|
|
* The Auth BO handles the rough edges and gaps between user/password authentication
|
|
* and OAuth via Chrome Identity API.
|
|
* Typical usage:
|
|
* var auth = new Auth(...);
|
|
* auth.setCredentials(...); // during the account setup
|
|
* auth.getEmailAddress(...); // called from the login controller to determine if there is already a user present on the device
|
|
* auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP, e.g. pinned intermediate certificates,
|
|
* username, password / oauth token, IMAP/SMTP server host names, ...
|
|
*/
|
|
var Auth = function(appConfigStore, oauth, pgp) {
|
|
this._appConfigStore = appConfigStore;
|
|
this._oauth = oauth;
|
|
this._pgp = pgp;
|
|
};
|
|
|
|
/**
|
|
* Retrieves credentials and IMAP/SMTP settings:
|
|
* 1) Fetches the credentials from disk, then...
|
|
* 2 a) ... in an oauth setting, retrieves a fresh oauth token from the Chrome Identity API.
|
|
* 2 b) ... in a user/passwd setting, does not need to do additional work.
|
|
* 3) Loads the intermediate certs from the configuration.
|
|
*
|
|
* @param {Function} callback(err, credentials)
|
|
*/
|
|
Auth.prototype.getCredentials = function(callback) {
|
|
var self = this;
|
|
|
|
if (!self.provider || !self.emailAddress) {
|
|
// we're not yet initialized, so let's load our stuff from disk
|
|
self._loadCredentials(function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
chooseLogin();
|
|
});
|
|
return;
|
|
}
|
|
|
|
chooseLogin();
|
|
|
|
function chooseLogin() {
|
|
if (self.provider === 'gmail' && !self.password) {
|
|
// oauth login for gmail
|
|
self.getOAuthToken(function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
done();
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (self.passwordNeedsDecryption) {
|
|
// decrypt password
|
|
self._pgp.decrypt(self.password, undefined, function(err, cleartext) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self.passwordNeedsDecryption = false;
|
|
self.password = cleartext;
|
|
|
|
done();
|
|
});
|
|
return;
|
|
}
|
|
|
|
done();
|
|
}
|
|
|
|
function done() {
|
|
var credentials = {
|
|
imap: {
|
|
secure: self.imap.secure,
|
|
port: self.imap.port,
|
|
host: self.imap.host,
|
|
ca: self.imap.ca,
|
|
pinned: self.imap.pinned,
|
|
auth: {
|
|
user: self.username,
|
|
xoauth2: self.oauthToken, // password or oauthToken is undefined
|
|
pass: self.password
|
|
}
|
|
},
|
|
smtp: {
|
|
secure: self.smtp.secure,
|
|
port: self.smtp.port,
|
|
host: self.smtp.host,
|
|
ca: self.smtp.ca,
|
|
pinned: self.smtp.pinned,
|
|
auth: {
|
|
user: self.username,
|
|
xoauth2: self.oauthToken,
|
|
pass: self.password // password or oauthToken is undefined
|
|
}
|
|
}
|
|
};
|
|
|
|
callback(null, credentials);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the credentials
|
|
*
|
|
* @param {String} options.provider The service provider, e.g. 'gmail', 'yahoo', 'tonline'. Matches the entry in the app-config.
|
|
* @param {String} options.emailAddress The email address
|
|
* @param {String} options.username The user name
|
|
* @param {String} options.realname The user's real name
|
|
* @param {String} options.password The password, only in user/passwd setting
|
|
* @param {String} options.smtp The smtp settings (host, port, secure)
|
|
* @param {String} options.imap The imap settings (host, port, secure)
|
|
*/
|
|
Auth.prototype.setCredentials = function(options) {
|
|
this.credentialsDirty = true;
|
|
this.provider = options.provider;
|
|
this.emailAddress = options.emailAddress;
|
|
this.username = options.username;
|
|
this.realname = options.realname ? options.realname : '';
|
|
this.password = options.password;
|
|
this.smtp = options.smtp; // host, port, secure, ca, pinned
|
|
this.imap = options.imap; // host, port, secure, ca, pinned
|
|
};
|
|
|
|
Auth.prototype.storeCredentials = function(callback) {
|
|
var self = this;
|
|
|
|
if (!self.credentialsDirty) {
|
|
return callback();
|
|
}
|
|
|
|
// persist the provider
|
|
self._appConfigStore.storeList([self.smtp], SMTP_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self._appConfigStore.storeList([self.imap], IMAP_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self._appConfigStore.storeList([self.provider], PROVIDER_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self._appConfigStore.storeList([self.username], USERNAME_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self._appConfigStore.storeList([self.realname], REALNAME_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (!self.password) {
|
|
self.credentialsDirty = false;
|
|
return callback();
|
|
}
|
|
|
|
if (self.passwordNeedsDecryption) {
|
|
// password is not decrypted yet, so no need to re-encrypt it before storing...
|
|
self._appConfigStore.storeList([self.password], PASSWD_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self.credentialsDirty = false;
|
|
callback();
|
|
});
|
|
return;
|
|
}
|
|
|
|
self._pgp.encrypt(self.password, undefined, function(err, ciphertext) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY, function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self.credentialsDirty = false;
|
|
callback();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns the email address. Loads it from disk, if necessary
|
|
*/
|
|
Auth.prototype.getEmailAddress = function(callback) {
|
|
var self = this;
|
|
|
|
if (self.emailAddress) {
|
|
return callback(null, {
|
|
emailAddress: self.emailAddress,
|
|
realname: self.realname
|
|
});
|
|
}
|
|
|
|
self._loadCredentials(function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
callback(null, {
|
|
emailAddress: self.emailAddress,
|
|
realname: self.realname
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* READ FIRST b/c usage of the oauth api is weird.
|
|
* the chrome identity api will let you query an oauth token for an email account without knowing
|
|
* the corresponding email address. also, android has multiple accounts whereas desktop chrome only
|
|
* has one user logged in.
|
|
* 1) try to read the email address from the configuration (see above)
|
|
* 2) fetch the oauth token. if we already HAVE an email address at this point, we can spare
|
|
* popping up the account picker on android! if not, the account picker will pop up. this
|
|
* is android only, since the desktop chrome will query the user that is logged into chrome
|
|
* 3) fetch the email address for the oauth token from the chrome identity api
|
|
*/
|
|
Auth.prototype.getOAuthToken = function(callback) {
|
|
var self = this;
|
|
|
|
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);
|
|
}
|
|
|
|
// shortcut if the email address is already known
|
|
if (self.emailAddress) {
|
|
self.oauthToken = oauthToken;
|
|
return callback();
|
|
}
|
|
|
|
// query the email address
|
|
self._oauth.queryEmailAddress(oauthToken, function(err, emailAddress) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self.oauthToken = oauthToken;
|
|
self.emailAddress = emailAddress;
|
|
callback();
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Loads email address, password, provider, ... from disk and sets them on `this`
|
|
*/
|
|
Auth.prototype._loadCredentials = function(callback) {
|
|
var self = this;
|
|
|
|
if (self.initialized) {
|
|
callback();
|
|
}
|
|
|
|
loadFromDB(SMTP_DB_KEY, function(err, smtp) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
|
|
loadFromDB(IMAP_DB_KEY, function(err, imap) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
|
|
loadFromDB(USERNAME_DB_KEY, function(err, username) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
|
|
loadFromDB(REALNAME_DB_KEY, function(err, realname) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
|
|
loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
loadFromDB(PASSWD_DB_KEY, function(err, password) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
loadFromDB(PROVIDER_DB_KEY, function(err, provider) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
self.emailAddress = emailAddress;
|
|
self.password = password;
|
|
self.passwordNeedsDecryption = !!password;
|
|
self.provider = provider;
|
|
self.username = username;
|
|
self.realname = realname;
|
|
self.smtp = smtp;
|
|
self.imap = imap;
|
|
self.initialized = true;
|
|
|
|
callback();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function loadFromDB(key, callback) {
|
|
self._appConfigStore.listItems(key, 0, null, function(err, cachedItems) {
|
|
callback(err, (!err && cachedItems && cachedItems[0]));
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles certificate updates and errors by notifying the user.
|
|
* @param {String} component Either imap or smtp
|
|
* @param {Function} callback The error handler
|
|
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate
|
|
*/
|
|
Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) {
|
|
var self = this;
|
|
|
|
axe.debug('new ssl certificate received: ' + pemEncodedCert);
|
|
|
|
if (!self[component].ca) {
|
|
// no previous ssl cert, trust on first use
|
|
self[component].ca = pemEncodedCert;
|
|
self.credentialsDirty = true;
|
|
self.storeCredentials(callback);
|
|
return;
|
|
}
|
|
|
|
if (self[component].ca === pemEncodedCert) {
|
|
// ignore multiple successive tls handshakes, e.g. for gmail
|
|
return;
|
|
}
|
|
|
|
if (self[component].ca && self[component].pinned) {
|
|
// do not update the pinned certificates!
|
|
callback({
|
|
title: str.outdatedCertificateTitle,
|
|
message: str.outdatedCertificateMessage.replace('{0}', self[component].host),
|
|
faqLink: str.certificateFaqLink,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// previous ssl cert known, does not match: query user and certificate
|
|
callback({
|
|
title: str.updateCertificateTitle,
|
|
message: str.updateCertificateMessage.replace('{0}', self[component].host),
|
|
positiveBtnStr: str.updateCertificatePosBtn,
|
|
negativeBtnStr: str.updateCertificateNegBtn,
|
|
showNegativeBtn: true,
|
|
faqLink: str.certificateFaqLink,
|
|
callback: function(granted) {
|
|
if (!granted) {
|
|
return;
|
|
}
|
|
|
|
self[component].ca = pemEncodedCert;
|
|
self.storeCredentials(function(err) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
onConnect(callback);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Logout of the app by clearing the app config store and in memory credentials
|
|
*/
|
|
Auth.prototype.logout = function(callback) {
|
|
var self = this;
|
|
|
|
// clear app config db
|
|
self._appConfigStore.clear(function(err) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
// clear in memory cache
|
|
self.setCredentials({});
|
|
self.initialized = undefined;
|
|
self.credentialsDirty = undefined;
|
|
self.passwordNeedsDecryption = undefined;
|
|
|
|
callback();
|
|
});
|
|
};
|
|
|
|
module.exports = Auth; |