VulpesOS/gaia/apps/email/js/ext/activesync/configurator.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

322 lines
10 KiB
JavaScript
Executable file

/**
* Configurator for activesync
**/
define(
[
'logic',
'../accountcommon',
'../a64',
'./account',
'../date',
'tcp-socket',
'require',
'exports'
],
function(
logic,
$accountcommon,
$a64,
$asacct,
$date,
tcpSocket,
require,
exports
) {
function checkServerCertificate(url, callback) {
var match = /^https:\/\/([^:/]+)(?::(\d+))?/.exec(url);
// probably unit test http case?
if (!match) {
callback(null);
return;
}
var port = match[2] ? parseInt(match[2], 10) : 443,
host = match[1];
console.log('checking', host, port, 'for security problem');
var sock = tcpSocket.open(host, port);
function reportAndClose(err) {
if (sock) {
var wasSock = sock;
sock = null;
try {
wasSock.close();
}
catch (ex) {
}
callback(err);
}
}
// this is a little dumb, but since we don't actually get an event right now
// that tells us when our secure connection is established, and connect always
// happens, we write data when we connect to help trigger an error or have us
// receive data to indicate we successfully connected.
// so, the deal is that connect is going to happen.
sock.onopen = function() {
sock.send(
new TextEncoder('utf-8').encode('GET /images/logo.png HTTP/1.1\n\n'));
};
sock.onerror = function(err) {
var reportErr = null;
if (err && typeof(err) === 'object' &&
/^Security/.test(err.name))
reportErr = 'bad-security';
reportAndClose(reportErr);
};
sock.ondata = function(data) {
reportAndClose(null);
};
}
var scope = logic.scope('ActivesyncConfigurator');
exports.account = $asacct;
exports.configurator = {
timeout: 30 * 1000,
_getFullDetailsFromAutodiscover: function($asproto, userDetails, url,
callback) {
logic(scope, 'autodiscover:begin', { url: url });
$asproto.raw_autodiscover(
url, userDetails.emailAddress, userDetails.password, self.timeout,
/* redirects are okay */ false,
function(error, config) {
if (error) {
var failureType = 'no-config-info',
failureDetails = {};
if (error instanceof $asproto.HttpError) {
if (error.status === 401)
failureType = 'bad-user-or-pass';
else if (error.status === 403)
failureType = 'not-authorized';
else
failureDetails.status = error.status;
}
else if (error instanceof $asproto.AutodiscoverDomainError) {
logic(scope, 'autodiscover.error', { message: error.message });
}
logic(scope, 'autodiscover:end', { url: url, err: failureType });
callback(failureType, null, failureDetails);
return;
}
logic(scope, 'autodiscover:end',
{ url: url, server: config.mobileSyncServer.url });
var autoconfig = {
type: 'activesync',
displayName: config.user.name,
incoming: {
server: config.mobileSyncServer.url,
username: config.user.email
},
};
callback(null, autoconfig, null);
});
},
/**
* There are 2 scenarios we can get invoked with:
* - Direct creation. We already know the ActiveSync endpoint. This happens
* from a hardcoded (for testing) or local (hotmail.com/outlook.com)
* autoconfig entry OR from a user typing that stuff in manually.
*
* - Indirection creation. We just know an AutoDiscover endpoing and need
* to run AutoDiscover. If our autoconfig process probed and found some
* AutoDiscover looking endpoints, that's how we end up here. It's also
* conceivable that in the future the manual config mode could use this
* path.
*/
tryToCreateAccount: function(universe, userDetails, domainInfo, callback) {
require(['activesync/protocol'], function ($asproto) {
if (domainInfo.incoming.autodiscoverEndpoint) {
this._getFullDetailsFromAutodiscover(
$asproto, userDetails, domainInfo.incoming.autodiscoverEndpoint,
function(err, fullConfigInfo, errDetails) {
// If we got an error, pass it directly back.
if (err) {
callback(err, fullConfigInfo, errDetails);
return;
}
// Otherwise we have a config and should continue the creation
// process.
this._createAccountUsingFullInfo(
universe, userDetails, fullConfigInfo, callback, $asproto);
}.bind(this));
return;
}
// We should have full config info then. Just call direct in.
this._createAccountUsingFullInfo(universe, userDetails, domainInfo,
callback, $asproto);
}.bind(this));
},
_createAccountUsingFullInfo: function(universe, userDetails, domainInfo,
callback, $asproto) {
logic(scope, 'create:begin', { server: domainInfo.incoming.server });
var credentials = {
username: domainInfo.incoming.username,
password: userDetails.password,
};
var deviceId = $asacct.makeUniqueDeviceId();
var self = this;
var conn = new $asproto.Connection(deviceId);
conn.open(domainInfo.incoming.server, credentials.username,
credentials.password);
conn.timeout = $asacct.DEFAULT_TIMEOUT_MS;
conn.connect(function(error, options) {
if (error) {
// This error is basically an indication of whether we were able to
// call getOptions or not. If the XHR request completed, we get an
// HttpError. If we timed out or an XHR error occurred, we get a
// general Error.
var failureType,
failureDetails = { server: domainInfo.incoming.server };
if (error instanceof $asproto.HttpError) {
if (error.status === 401) {
failureType = 'bad-user-or-pass';
}
else if (error.status === 403) {
failureType = 'not-authorized';
}
// Treat any other errors where we talked to the server as a problem
// with the server.
else {
failureType = 'server-problem';
failureDetails.status = error.status;
}
}
else {
// We didn't talk to the server, so it's either an unresponsive
// server or a server with a bad certificate. (We require https
// outside of unit tests so there's no need to branch here.)
checkServerCertificate(
domainInfo.incoming.server,
function(securityError) {
var failureType;
if (securityError)
failureType = 'bad-security';
else
failureType = 'unresponsive-server';
callback(failureType, null, failureDetails);
});
return;
}
logic(scope, 'create:end', {
server: domainInfo.incoming.server,
err: failureType
});
callback(failureType, null, failureDetails);
return;
}
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var accountDef = {
id: accountId,
name: userDetails.accountName || userDetails.emailAddress,
defaultPriority: $date.NOW(),
type: 'activesync',
syncRange: 'auto',
syncInterval: userDetails.syncInterval || 0,
notifyOnNew: userDetails.hasOwnProperty('notifyOnNew') ?
userDetails.notifyOnNew : true,
playSoundOnSend: userDetails.hasOwnProperty('playSoundOnSend') ?
userDetails.playSoundOnSend : true,
credentials: credentials,
connInfo: {
server: domainInfo.incoming.server,
deviceId: deviceId
},
identities: [
{
id: accountId + '/' +
$a64.encodeInt(universe.config.nextIdentityNum++),
name: userDetails.displayName || domainInfo.displayName,
address: userDetails.emailAddress,
replyTo: null,
signature: null
},
]
};
logic(scope, 'create:end', {
server: domainInfo.incoming.server,
id: accountId
});
self._loadAccount(universe, accountDef, conn, function (account) {
callback(null, account, null);
});
});
},
recreateAccount: function cfg_as_ra(universe, oldVersion, oldAccountInfo,
callback) {
var oldAccountDef = oldAccountInfo.def;
var credentials = {
username: oldAccountDef.credentials.username,
password: oldAccountDef.credentials.password,
};
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var accountDef = {
id: accountId,
name: oldAccountDef.name,
type: 'activesync',
syncRange: oldAccountDef.syncRange,
syncInterval: oldAccountDef.syncInterval || 0,
notifyOnNew: oldAccountDef.hasOwnProperty('notifyOnNew') ?
oldAccountDef.notifyOnNew : true,
playSoundOnSend: oldAccountDef.hasOwnProperty('playSoundOnSend') ?
oldAccountDef.playSoundOnSend : true,
credentials: credentials,
connInfo: {
server: oldAccountDef.connInfo.server,
deviceId: oldAccountDef.connInfo.deviceId ||
$asacct.makeUniqueDeviceId()
},
identities: $accountcommon.recreateIdentities(universe, accountId,
oldAccountDef.identities)
};
this._loadAccount(universe, accountDef, null, function (account) {
callback(null, account, null);
});
},
/**
* Save the account def and folder info for our new (or recreated) account and
* then load it.
*/
_loadAccount: function cfg_as__loadAccount(universe, accountDef,
protoConn, callback) {
// XXX: Just reload the old folders when applicable instead of syncing the
// folder list again, which is slow.
var folderInfo = {
$meta: {
nextFolderNum: 0,
nextMutationNum: 0,
lastFolderSyncAt: 0,
syncKey: '0',
},
$mutations: [],
$mutationState: {},
};
universe.saveAccountDef(accountDef, folderInfo);
universe._loadAccount(accountDef, folderInfo, protoConn, callback);
},
};
}); // end define