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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,322 @@
/**
* 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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,472 @@
define(
[
'logic',
'mix',
'../jobmixins',
'../drafts/jobs',
'activesync/codepages/AirSync',
'activesync/codepages/Email',
'activesync/codepages/Move',
'module',
'require',
'exports'
],
function(
logic,
mix,
$jobmixins,
draftsJobs,
$AirSync,
$Email,
$Move,
$module,
require,
exports
) {
'use strict';
var $wbxml;
function lazyConnection(cbIndex, fn, failString) {
return function lazyRun() {
var args = Array.slice(arguments),
errback = args[cbIndex],
self = this;
require(['wbxml'], function (wbxml) {
if (!$wbxml) {
$wbxml = wbxml;
}
self.account.withConnection(errback, function () {
fn.apply(self, args);
}, failString);
});
};
}
function ActiveSyncJobDriver(account, state) {
this.account = account;
// XXX for simplicity for now, let's assume that ActiveSync GUID's are
// maintained across folder moves.
this.resilientServerIds = true;
this._heldMutexReleasers = [];
logic.defineScope(this, 'ActiveSyncJobDriver',
{ accountId: this.account.id });
this._state = state;
// (we only need to use one as a proxy for initialization)
if (!state.hasOwnProperty('suidToServerId')) {
state.suidToServerId = {};
state.moveMap = {};
}
this._stateDelta = {
serverIdMap: null,
moveMap: null,
};
}
exports.ActiveSyncJobDriver = ActiveSyncJobDriver;
ActiveSyncJobDriver.prototype = {
//////////////////////////////////////////////////////////////////////////////
// helpers
postJobCleanup: $jobmixins.postJobCleanup,
allJobsDone: $jobmixins.allJobsDone,
_accessFolderForMutation: function(folderId, needConn, callback, deathback,
label) {
var storage = this.account.getFolderStorageForFolderId(folderId),
self = this;
storage.runMutexed(label, function(releaseMutex) {
self._heldMutexReleasers.push(releaseMutex);
var syncer = storage.folderSyncer;
if (needConn) {
self.account.withConnection(callback, function () {
try {
callback(syncer.folderConn, storage);
}
catch (ex) {
logic(self, 'callbackErr', { ex: ex });
}
});
} else {
try {
callback(syncer.folderConn, storage);
}
catch (ex) {
logic(self, 'callbackErr', { ex: ex });
}
}
});
},
_partitionAndAccessFoldersSequentially:
$jobmixins._partitionAndAccessFoldersSequentially,
//////////////////////////////////////////////////////////////////////////////
// modtags
local_do_modtags: $jobmixins.local_do_modtags,
do_modtags: lazyConnection(1, function(op, jobDoneCallback, undo) {
// Note: this method is derived from the IMAP implementation.
var addTags = undo ? op.removeTags : op.addTags,
removeTags = undo ? op.addTags : op.removeTags;
function getMark(tag) {
if (addTags && addTags.indexOf(tag) !== -1)
return true;
if (removeTags && removeTags.indexOf(tag) !== -1)
return false;
return undefined;
}
var markRead = getMark('\\Seen');
var markFlagged = getMark('\\Flagged');
var as = $AirSync.Tags;
var em = $Email.Tags;
var aggrErr = null;
this._partitionAndAccessFoldersSequentially(
op.messages, true,
function perFolder(folderConn, storage, serverIds, namers, callWhenDone) {
var modsToGo = 0;
function tagsModded(err) {
if (err) {
console.error('failure modifying tags', err);
aggrErr = 'unknown';
return;
}
op.progress += (undo ? -serverIds.length : serverIds.length);
if (--modsToGo === 0)
callWhenDone();
}
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
folderConn.performMutation(
function withWriter(w) {
for (var i = 0; i < serverIds.length; i++) {
w.stag(as.Change)
.tag(as.ServerId, serverIds[i])
.stag(as.ApplicationData);
if (markRead !== undefined)
w.tag(em.Read, markRead ? '1' : '0');
if (markFlagged !== undefined)
w.stag(em.Flag)
.tag(em.Status, markFlagged ? '2' : '0')
.etag();
w.etag(as.ApplicationData)
.etag(as.Change);
}
},
function mutationPerformed(err) {
if (err)
aggrErr = err;
callWhenDone();
});
},
function allDone() {
jobDoneCallback(aggrErr);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
/* reverse if we're undoing */ undo,
'modtags');
}),
check_modtags: function(op, callback) {
callback(null, 'idempotent');
},
local_undo_modtags: $jobmixins.local_undo_modtags,
undo_modtags: function(op, callback) {
this.do_modtags(op, callback, true);
},
//////////////////////////////////////////////////////////////////////////////
// move
local_do_move: $jobmixins.local_do_move,
do_move: lazyConnection(1, function(op, jobDoneCallback) {
/*
* The ActiveSync command for this does not produce or consume SyncKeys.
* As such, we don't need to acquire mutexes for the source folders for
* synchronization correctness, although it is helpful for ordering
* purposes and reducing confusion.
*
* For the target folder a similar logic exists as long as the server-issued
* GUID's are resilient against folder moves. However, we do require in
* all cases that before synchronizing the target folder that we make sure
* all move operations to the folder have completed so we message doesn't
* disappear and then show up again. XXX we are not currently enforcing this
* yet.
*/
var aggrErr = null, account = this.account,
targetFolderStorage = this.account.getFolderStorageForFolderId(
op.targetFolder);
var mo = $Move.Tags;
this._partitionAndAccessFoldersSequentially(
op.messages, true,
function perFolder(folderConn, storage, serverIds, namers, callWhenDone) {
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(mo.MoveItems);
for (var i = 0; i < serverIds.length; i++) {
w.stag(mo.Move)
.tag(mo.SrcMsgId, serverIds[i])
.tag(mo.SrcFldId, storage.folderMeta.serverId)
.tag(mo.DstFldId, targetFolderStorage.folderMeta.serverId)
.etag(mo.Move);
}
w.etag(mo.MoveItems);
account.conn.postCommand(w, function(err, response) {
if (err) {
aggrErr = err;
console.error('failure moving messages:', err);
}
callWhenDone();
});
},
function allDone() {
jobDoneCallback(aggrErr, null, true);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
false,
'move');
}),
check_move: function(op, jobDoneCallback) {
},
local_undo_move: $jobmixins.local_undo_move,
undo_move: function(op, jobDoneCallback) {
},
//////////////////////////////////////////////////////////////////////////////
// delete
local_do_delete: $jobmixins.local_do_delete,
do_delete: lazyConnection(1, function(op, jobDoneCallback) {
var aggrErr = null;
var as = $AirSync.Tags;
var em = $Email.Tags;
this._partitionAndAccessFoldersSequentially(
op.messages, true,
function perFolder(folderConn, storage, serverIds, namers, callWhenDone) {
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
folderConn.performMutation(
function withWriter(w) {
for (var i = 0; i < serverIds.length; i++) {
w.stag(as.Delete)
.tag(as.ServerId, serverIds[i])
.etag(as.Delete);
}
},
function mutationPerformed(err) {
if (err) {
aggrErr = err;
console.error('failure deleting messages:', err);
}
callWhenDone();
});
},
function allDone() {
jobDoneCallback(aggrErr, null, true);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
false,
'delete');
}),
check_delete: function(op, callback) {
callback(null, 'idempotent');
},
local_undo_delete: $jobmixins.local_undo_delete,
// TODO implement
undo_delete: function(op, callback) {
callback('moot');
},
//////////////////////////////////////////////////////////////////////////////
// syncFolderList
//
// Synchronize our folder list. This should always be an idempotent operation
// that makes no sense to undo/redo/etc.
local_do_syncFolderList: function(op, doneCallback) {
doneCallback(null);
},
do_syncFolderList: lazyConnection(1, function(op, doneCallback) {
var account = this.account, self = this;
// The inbox needs to be resynchronized if there was no server id and we
// have active slices displaying the contents of the folder. (No server id
// means the sync will not happen.)
var inboxFolder = account.getFirstFolderWithType('inbox'),
inboxStorage;
if (inboxFolder && inboxFolder.serverId === null)
inboxStorage = account.getFolderStorageForFolderId(inboxFolder.id);
account.syncFolderList(function(err) {
if (!err)
account.meta.lastFolderSyncAt = Date.now();
// save if it worked
doneCallback(err ? 'aborted-retry' : null, null, !err);
if (inboxStorage && inboxStorage.hasActiveSlices) {
if (!err) {
console.log("Refreshing fake inbox");
inboxStorage.resetAndRefreshActiveSlices();
}
// XXX: If we do have an error here, we should probably report
// syncfailed on the slices to let the user retry. However, what needs
// retrying is syncFolderList, not syncing the messages in a folder.
// Since that's complicated to handle, and syncFolderList will retry
// automatically, we can ignore that case for now.
}
});
}, 'aborted-retry'),
check_syncFolderList: function(op, doneCallback) {
doneCallback(null, 'coherent-notyet');
},
local_undo_syncFolderList: function(op, doneCallback) {
doneCallback('moot');
},
undo_syncFolderList: function(op, doneCallback) {
doneCallback('moot');
},
//////////////////////////////////////////////////////////////////////////////
// downloadBodies: Download the bodies from a list of messages
local_do_downloadBodies: $jobmixins.local_do_downloadBodies,
do_downloadBodies: $jobmixins.do_downloadBodies,
check_downloadBodies: $jobmixins.check_downloadBodies,
//////////////////////////////////////////////////////////////////////////////
// downloadBodyReps: Download the bodies from a single message
local_do_downloadBodyReps: $jobmixins.local_do_downloadBodyReps,
do_downloadBodyReps: $jobmixins.do_downloadBodyReps,
check_downloadBodyReps: $jobmixins.check_downloadBodyReps,
//////////////////////////////////////////////////////////////////////////////
// download: Download one or more attachments from a single message
local_do_download: $jobmixins.local_do_download,
do_download: $jobmixins.do_download,
check_download: $jobmixins.check_download,
local_undo_download: $jobmixins.local_undo_download,
undo_download: $jobmixins.undo_download,
//////////////////////////////////////////////////////////////////////////////
local_do_sendOutboxMessages: $jobmixins.local_do_sendOutboxMessages,
do_sendOutboxMessages: $jobmixins.do_sendOutboxMessages,
check_sendOutboxMessages: $jobmixins.check_sendOutboxMessages,
local_undo_sendOutboxMessages: $jobmixins.local_undo_sendOutboxMessages,
undo_sendOutboxMessages: $jobmixins.undo_sendOutboxMessages,
local_do_setOutboxSyncEnabled: $jobmixins.local_do_setOutboxSyncEnabled,
// upgrade: perfom necessary upgrades when the db version changes
local_do_upgradeDB: $jobmixins.local_do_upgradeDB,
//////////////////////////////////////////////////////////////////////////////
// purgeExcessMessages is a NOP for activesync
local_do_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
do_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
check_purgeExcessMessages: function(op, doneCallback) {
return 'idempotent';
},
local_undo_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
undo_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
//////////////////////////////////////////////////////////////////////////////
};
mix(ActiveSyncJobDriver.prototype, draftsJobs.draftsMixins);
}); // end define