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

View file

@ -0,0 +1,199 @@
define(function() {
'use strict';
function debug(str) {
//dump('ConfigParser: ' + str + '\n');
}
function nsResolver(prefix) {
var baseUrl = 'http://schemas.microsoft.com/exchange/autodiscover/';
var ns = {
rq: baseUrl + 'mobilesync/requestschema/2006',
ad: baseUrl + 'responseschema/2006',
ms: baseUrl + 'mobilesync/responseschema/2006',
};
return ns[prefix] || null;
}
function parseAccountCommon(uid, cmd, text) {
var doc = new DOMParser().parseFromString(text, 'text/xml');
var getNode = function(xpath, rel) {
return doc.evaluate(xpath, rel || doc, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue;
};
var dictifyChildNodes = function(node) {
if (!node) {
return null;
}
var dict = {};
for (var kid = node.firstElementChild; kid;
kid = kid.nextElementSibling) {
dict[kid.tagName] = kid.textContent;
}
return dict;
};
var provider = getNode('/clientConfig/emailProvider');
// Get the first incomingServer we can use (we assume first == best).
var incoming = getNode('incomingServer[@type="imap"] | ' +
'incomingServer[@type="activesync"] | ' +
'incomingServer[@type="pop3"]', provider);
var outgoing = getNode('outgoingServer[@type="smtp"]', provider);
var oauth2Settings = dictifyChildNodes(getNode('oauth2Settings', provider));
var config = null;
var status = null;
if (incoming) {
config = {
type: null,
incoming: {},
outgoing: {},
oauth2Settings: oauth2Settings
};
for (var iter in Iterator(incoming.children)) {
var child = iter[1];
config.incoming[child.tagName] = child.textContent;
}
if (incoming.getAttribute('type') === 'activesync') {
config.type = 'activesync';
} else if (outgoing) {
var isImap = incoming.getAttribute('type') === 'imap';
config.type = isImap ? 'imap+smtp' : 'pop3+smtp';
for (var iter in Iterator(outgoing.children)) {
var child = iter[1];
config.outgoing[child.tagName] = child.textContent;
}
var ALLOWED_SOCKET_TYPES = ['SSL', 'STARTTLS'];
// We do not support unencrypted connections outside of unit tests.
if (ALLOWED_SOCKET_TYPES.indexOf(config.incoming.socketType) === -1 ||
ALLOWED_SOCKET_TYPES.indexOf(config.outgoing.socketType) === -1) {
config = null;
status = 'unsafe';
}
} else {
config = null;
status = 'no-outgoing';
}
} else {
status = 'no-incoming';
}
self.sendMessage(uid, cmd, [config, status]);
}
function parseActiveSyncAccount(uid, cmd, text, aNoRedirect) {
var doc = new DOMParser().parseFromString(text, 'text/xml');
var getNode = function(xpath, rel) {
return doc.evaluate(xpath, rel, nsResolver,
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue;
};
var getNodes = function(xpath, rel) {
return doc.evaluate(xpath, rel, nsResolver,
XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
};
var getString = function(xpath, rel) {
return doc.evaluate(xpath, rel, nsResolver, XPathResult.STRING_TYPE,
null).stringValue;
};
var postResponse = function(error, config, redirectedEmail) {
self.sendMessage(uid, cmd, [config, error, redirectedEmail]);
};
var error = null;
if (doc.documentElement.tagName === 'parsererror') {
error = 'Error parsing autodiscover response';
return postResponse(error);
}
// Note: specs seem to indicate the root should be ms:Autodiscover too.
// It's not clear why we were using ad:Autodiscover or if it ever worked,
// but there's no meaningful cost to leave that around.
var responseNode = getNode('/ad:Autodiscover/ms:Response', doc) ||
getNode('/ms:Autodiscover/ms:Response', doc);
if (!responseNode) {
error = 'Missing Autodiscover Response node';
return postResponse(error);
}
var error = getNode('ms:Error', responseNode) ||
getNode('ms:Action/ms:Error', responseNode);
if (error) {
error = getString('ms:Message/text()', error);
return postResponse(error);
}
var redirect = getNode('ms:Action/ms:Redirect', responseNode);
if (redirect) {
if (aNoRedirect) {
error = 'Multiple redirects occurred during autodiscovery';
return postResponse(error);
}
var redirectedEmail = getString('text()', redirect);
return postResponse(null, null, redirectedEmail);
}
var user = getNode('ms:User', responseNode);
var config = {
culture: getString('ms:Culture/text()', responseNode),
user: {
name: getString('ms:DisplayName/text()', user),
email: getString('ms:EMailAddress/text()', user),
},
servers: [],
};
var servers = getNodes('ms:Action/ms:Settings/ms:Server', responseNode);
var server;
while ((server = servers.iterateNext())) {
config.servers.push({
type: getString('ms:Type/text()', server),
url: getString('ms:Url/text()', server),
name: getString('ms:Name/text()', server),
serverData: getString('ms:ServerData/text()', server),
});
}
// Try to find a MobileSync server from Autodiscovery.
for (var iter in Iterator(config.servers)) {
var server = iter[1];
if (server.type === 'MobileSync') {
config.mobileSyncServer = server;
break;
}
}
if (!config.mobileSyncServer) {
error = 'No MobileSync server found';
return postResponse(error, config);
}
postResponse(null, config);
};
var self = {
name: 'configparser',
sendMessage: null,
process: function(uid, cmd, args) {
debug('process ' + cmd);
switch (cmd) {
case 'accountcommon':
parseAccountCommon(uid, cmd, args[0]);
break;
case 'accountactivesync':
parseActiveSyncAccount(uid, cmd, args[0], args[1]);
break;
}
}
};
return self;
});

View file

@ -0,0 +1,295 @@
/*jshint browser: true */
/*global define, console */
define(function(require) {
'use strict';
var evt = require('evt');
function debug(str) {
console.log('cronsync-main: ' + str);
}
function makeData(accountIds, interval, date) {
return {
type: 'sync',
accountIds: accountIds,
interval: interval,
timestamp: date.getTime()
};
}
// Creates a string key from an array of string IDs. Uses a space
// separator since that cannot show up in an ID.
function makeAccountKey(accountIds) {
return 'id' + accountIds.join(' ');
}
// Converts 'interval' + intervalInMillis to just a intervalInMillis
// Number.
var prefixLength = 'interval'.length;
function toInterval(intervalKey) {
return parseInt(intervalKey.substring(prefixLength), 10);
}
// Makes sure two arrays have the same values, account IDs.
function hasSameValues(ary1, ary2) {
if (ary1.length !== ary2.length) {
return false;
}
var hasMismatch = ary1.some(function(item, i) {
return item !== ary2[i];
});
return !hasMismatch;
}
var dispatcher = {
_routeReady: false,
_routeQueue: [],
_sendMessage: function(type, args) {
if (this._routeReady) {
// sendMessage is added to routeRegistration by the main-router module.
routeRegistration.sendMessage(null, type, args);
} else {
this._routeQueue.push([type, args]);
}
},
/**
* Called by worker side to indicate it can now receive messages.
*/
hello: function() {
this._routeReady = true;
if (this._routeQueue.length) {
var queue = this._routeQueue;
this._routeQueue = [];
queue.forEach(function(args) {
this._sendMessage(args[0], args[1]);
}.bind(this));
}
},
/**
* Clears all sync-based alarms. Normally not called, except perhaps for
* tests or debugging.
*/
clearAll: function() {
var mozAlarms = navigator.mozAlarms;
if (!mozAlarms) {
return;
}
var r = mozAlarms.getAll();
r.onsuccess = function(event) {
var alarms = event.target.result;
if (!alarms) {
return;
}
alarms.forEach(function(alarm) {
if (alarm.data && alarm.data.type === 'sync') {
mozAlarms.remove(alarm.id);
}
});
}.bind(this);
r.onerror = function(err) {
console.error('cronsync-main clearAll mozAlarms.getAll: error: ' +
err);
}.bind(this);
},
/**
* Makes sure there is an alarm set for every account in the list.
* @param {Object} syncData. An object with keys that are 'interval' +
* intervalInMilliseconds, and values are arrays of account IDs that should
* be synced at that interval.
*/
ensureSync: function (syncData) {
var mozAlarms = navigator.mozAlarms;
if (!mozAlarms) {
console.warn('no mozAlarms support!');
return;
}
debug('ensureSync called');
var request = mozAlarms.getAll();
request.onsuccess = function(event) {
debug('success!');
var alarms = event.target.result;
// If there are no alarms a falsey value may be returned. We want
// to not die and also make sure to signal we completed, so just make
// an empty list.
if (!alarms) {
alarms = [];
}
// Find all IDs being tracked by alarms
var expiredAlarmIds = [],
okAlarmIntervals = {},
uniqueAlarms = {};
alarms.forEach(function(alarm) {
// Only care about sync alarms.
if (!alarm.data || !alarm.data.type || alarm.data.type !== 'sync') {
return;
}
var intervalKey = 'interval' + alarm.data.interval,
wantedAccountIds = syncData[intervalKey];
if (!wantedAccountIds || !hasSameValues(wantedAccountIds,
alarm.data.accountIds)) {
debug('account array mismatch, canceling existing alarm');
expiredAlarmIds.push(alarm.id);
} else {
// Confirm the existing alarm is still good.
var interval = toInterval(intervalKey),
now = Date.now(),
alarmTime = alarm.data.timestamp,
accountKey = makeAccountKey(wantedAccountIds);
// If the interval is nonzero, and there is no other alarm found
// for that account combo, and if it is not in the past and if it
// is not too far in the future, it is OK to keep.
if (interval && !uniqueAlarms.hasOwnProperty(accountKey) &&
alarmTime > now && alarmTime < now + interval) {
debug('existing alarm is OK: ' + accountKey);
uniqueAlarms[accountKey] = true;
okAlarmIntervals[intervalKey] = true;
} else {
debug('existing alarm is out of interval range, canceling');
expiredAlarmIds.push(alarm.id);
}
}
});
expiredAlarmIds.forEach(function(alarmId) {
mozAlarms.remove(alarmId);
});
var alarmMax = 0,
alarmCount = 0,
self = this;
// Called when alarms are confirmed to be set.
function done() {
alarmCount += 1;
if (alarmCount < alarmMax) {
return;
}
debug('ensureSync completed');
// Indicate ensureSync has completed because the
// back end is waiting to hear alarm was set before
// triggering sync complete.
self._sendMessage('syncEnsured');
}
Object.keys(syncData).forEach(function(intervalKey) {
// Skip if the existing alarm is already good.
if (okAlarmIntervals.hasOwnProperty(intervalKey)) {
return;
}
var interval = toInterval(intervalKey),
accountIds = syncData[intervalKey],
date = new Date(Date.now() + interval);
// Do not set an timer for a 0 interval, bad things happen.
if (!interval) {
return;
}
alarmMax += 1;
var alarmRequest = mozAlarms.add(date, 'ignoreTimezone',
makeData(accountIds, interval, date));
alarmRequest.onsuccess = function() {
debug('success: mozAlarms.add for ' + 'IDs: ' + accountIds +
' at ' + interval + 'ms');
done();
};
alarmRequest.onerror = function(err) {
console.error('cronsync-main mozAlarms.add for IDs: ' +
accountIds +
' failed: ' + err);
};
});
// If no alarms were added, indicate ensureSync is done.
if (!alarmMax) {
done();
}
}.bind(this);
request.onerror = function(err) {
console.error('cronsync-main ensureSync mozAlarms.getAll: error: ' +
err);
};
}
};
if (navigator.mozSetMessageHandler) {
navigator.mozSetMessageHandler('alarm', function onAlarm(alarm) {
console.log('mozSetMessageHandler: received an alarm');
// Important for gaia email app to know when a mozSetMessageHandler has
// been dispatched. Could be removed if notification close events did not
// open the email app, or if we wanted to be less efficient on closing
// down the email app on those events. Although the email app would not be
// a good memory citizen in that case.
if (window.hasOwnProperty('appDispatchedMessage')) {
window.appDispatchedMessage = true;
}
// Do not bother with alarms that are not sync alarms.
var data = alarm.data;
if (!data || data.type !== 'sync') {
return;
}
// Need to acquire the wake locks during this notification
// turn of the event loop -- later turns are not guaranteed to
// be up and running. However, knowing when to release the locks
// is only known to the front end, so publish event about it.
// Need a CPU lock since otherwise the app can be paused
// mid-function, which could lead to unexpected behavior, and the
// sync should be completed as quick as possible to then close
// down the app.
// TODO: removed wifi wake lock due to network complications, to
// be addressed in a separate changset.
if (navigator.requestWakeLock) {
var locks = [
navigator.requestWakeLock('cpu')
];
debug('wake locks acquired: ' + locks +
' for account IDs: ' + data.accountIds);
evt.emitWhenListener('cronSyncWakeLocks',
makeAccountKey(data.accountIds), locks);
}
debug('alarm dispatch started at ' + (new Date()));
dispatcher._sendMessage('alarm',
[data.accountIds, data.interval]);
});
}
var routeRegistration = {
name: 'cronsync',
sendMessage: null,
dispatch: dispatcher
};
return routeRegistration;
});

View file

@ -0,0 +1,96 @@
define(function() {
'use strict';
function debug(str) {
dump('DeviceStorage: ' + str + '\n');
}
function save(uid, cmd, storage, blob, filename, registerDownload) {
// For the download manager, we want to avoid the composite storage
var deviceStorage = navigator.getDeviceStorage(storage);
if (!deviceStorage) {
self.sendMessage(uid, cmd, [false, 'no-device-storage', null, false]);
return;
}
var req = deviceStorage.addNamed(blob, filename);
req.onerror = function() {
self.sendMessage(uid, cmd, [false, req.error.name, null, false]);
};
req.onsuccess = function(e) {
var prefix = '';
if (typeof window.IS_GELAM_TEST !== 'undefined') {
prefix = 'TEST_PREFIX/';
}
var savedPath = prefix + e.target.result;
var registering = false;
if (registerDownload) {
var downloadManager = navigator.mozDownloadManager;
console.warn('have downloadManager?', !!downloadManager,
'have adoptDownload?', downloadManager && !!downloadManager.adoptDownload);
if (downloadManager && downloadManager.adoptDownload) {
try {
var fullPath = e.target.result;
var firstSlash = fullPath.indexOf('/', 2); // ignore leading /
var storageName = fullPath.substring(1, firstSlash); // eat 1st /
var storagePath = fullPath.substring(firstSlash + 1);
console.log('adopting download', deviceStorage.storageName,
e.target.result);
registering = true;
downloadManager.adoptDownload({
totalBytes: blob.size,
// There's no useful URL we can provide; anything would be an
// internal URI scheme that we can't service.
url: '',
storageName: storageName,
storagePath: storagePath,
contentType: blob.type,
// The time we started isn't inherently interesting given that the
// entirety of the file appears instantaneously to the download
// manager, now is good enough.
startTime: new Date(Date.now()),
}).then(function() {
console.log('registered download with download manager');
self.sendMessage(uid, cmd, [true, null, savedPath, true]);
}, function() {
console.warn('failed to register download with download manager');
self.sendMessage(uid, cmd, [true, null, savedPath, false]);
});
} catch (ex) {
console.error('Problem adopting download!:', ex, '\n', ex.stack);
}
} else {
console.log('download manager not available, not registering.');
}
} else {
console.log('do not want to register download');
}
// Bool success, String err, String filename
if (!registering) {
self.sendMessage(uid, cmd, [true, null, savedPath, false]);
}
};
}
var self = {
name: 'devicestorage',
sendMessage: null,
process: function(uid, cmd, args) {
debug('process ' + cmd);
switch (cmd) {
case 'save':
save(uid, cmd, args[0], args[1], args[2], args[3]);
break;
}
}
};
return self;
});

View file

@ -0,0 +1,522 @@
define(function() {
'use strict';
function debug(str) {
dump('MailDB: ' + str + '\n');
}
var db = null;
function open(uid, cmd, args) {
db = self._debugDB = new MailDB(args[0], function() {
self.sendMessage(uid, cmd, Array.prototype.slice.call(arguments));
});
}
function others(uid, cmd, args) {
if (!Array.isArray(args))
args = [];
args.push(function() {
self.sendMessage(uid, cmd, Array.prototype.slice.call(arguments));
});
if (!db._db)
console.warn('trying to call', cmd, 'on apparently dead db. skipping.');
else
db[cmd].apply(db, args);
}
var self = {
name: 'maildb',
sendMessage: null,
process: function(uid, cmd, args) {
switch (cmd) {
case 'open':
open(uid, cmd, args);
break;
default:
others(uid, cmd, args);
break;
}
},
_debugDB: null
};
var IndexedDB;
if (("indexedDB" in window) && window.indexedDB) {
IndexedDB = window.indexedDB;
} else if (("mozIndexedDB" in window) && window.mozIndexedDB) {
IndexedDB = window.mozIndexedDB;
} else if (("webkitIndexedDB" in window) && window.webkitIndexedDB) {
IndexedDB = window.webkitIndexedDB;
} else {
console.error("No IndexedDB!");
throw new Error("I need IndexedDB; load me in a content page universe!");
}
/**
* The current database version.
*
* Explanation of most recent bump:
*
* Bumping to 22 because of account changes around cronsyncing, an "undefined"
* error with summaries and some constant changes.
*
* Bumping to 21 because of massive error in partial fetching merges.
*
* Bumping to 20 because of block sizing changes.
*
* Bumping to 19 because of change from uids to ids, but mainly because we are
* now doing parallel IMAP fetching and we want to see the results of using it
* immediately.
*
* Bumping to 18 because of massive change for lazily fetching snippets and
* message bodies.
*
* Bumping to 17 because we changed the folder representation to store
* hierarchy.
*/
var CUR_VERSION = 22;
/**
* What is the lowest database version that we are capable of performing a
* friendly-but-lazy upgrade where we nuke the database but re-create the user's
* accounts? Set this to the CUR_VERSION if we can't.
*
* Note that this type of upgrade can still be EXTREMELY DANGEROUS because it
* may blow away user actions that haven't hit a server yet.
*/
var FRIENDLY_LAZY_DB_UPGRADE_VERSION = 5;
/**
* The configuration table contains configuration data that should persist
* despite implementation changes. Global configuration data, and account login
* info. Things that would be annoying for us to have to re-type.
*/
var TBL_CONFIG = 'config',
CONFIG_KEY_ROOT = 'config',
// key: accountDef:`AccountId`
CONFIG_KEYPREFIX_ACCOUNT_DEF = 'accountDef:';
/**
* The folder-info table stores meta-data about the known folders for each
* account. This information may be blown away on upgrade.
*
* While we may eventually stash info like histograms of messages by date in
* a folder, for now this is all about serving as a directory service for the
* header and body blocks. See `ImapFolderStorage` for the details of the
* payload.
*
* All the folder info for each account is stored in a single object since we
* keep it all in-memory for now.
*
* key: `AccountId`
*/
var TBL_FOLDER_INFO = 'folderInfo';
/**
* Stores time-clustered information about messages in folders. Message bodies
* and attachment names are not included, but initial snippets and the presence
* of attachments are.
*
* We store headers separately from bodies because our access patterns are
* different for each. When we want headers, all we want is headers, and don't
* need the bodies clogging up our IO. Additionally, we expect better
* compression for bodies if they are stored together.
*
* key: `FolderId`:`BlockId`
*
* Each value is an object dictionary whose keys are either UIDs or a more
* globally unique identifier (ex: gmail's X-GM-MSGID values). The values are
* the info on the message; see `ImapFolderStorage` for details.
*/
var TBL_HEADER_BLOCKS = 'headerBlocks';
/**
* Stores time-clustered information about message bodies. Body details include
* the list of attachments, as well as the body payloads and the embedded inline
* parts if they all met the sync heuristics. (If we can't sync all the inline
* images, for example, we won't sync any.)
*
* Note that body blocks are not paired with header blocks; their storage is
* completely separate.
*
* key: `FolderId`:`BlockId`
*
* Each value is an object dictionary whose keys are either UIDs or a more
* globally unique identifier (ex: gmail's X-GM-MSGID values). The values are
* the info on the message; see `ImapFolderStorage` for details.
*/
var TBL_BODY_BLOCKS = 'bodyBlocks';
/**
* DB helper methods for Gecko's IndexedDB implementation. We are assuming
* the presence of the Mozilla-specific mozGetAll helper right now. Since our
* app is also dependent on the existence of the TCP API that no one else
* supports right now and we are assuming a SQLite-based IndexedDB
* implementation, this does not seem too crazy.
*
* == Useful tidbits on our IndexedDB implementation
*
* - SQLite page size is 32k
* - The data persisted to the database (but not Blobs AFAICS) gets compressed
* using snappy on a per-value basis.
* - Blobs/files are stored as files on the file-system that are referenced by
* the data row. Since they are written in one go, they are highly unlikely
* to be fragmented.
* - Blobs/files are clever once persisted. Specifically, nsDOMFileFile
* instances are created with just the knowledge of the file-path. This means
* the data does not have to be marshaled, and it means that it can be
* streamed off the disk. This is primarily beneficial in that if there is
* data we don't need to mutate, we can feed it directly to the web browser
* engine without potentially creating JS string garbage.
*
* Given the page size and snappy compression, we probably only want to spill to
* a blob for non-binary data that exceeds 64k by a fair margin, and less
* compressible binary data that is at least 64k.
*
* @args[
* @param[testOptions #:optional @dict[
* @key[dbVersion #:optional Number]{
* Override the database version to treat as the database version to use.
* This is intended to let us do simple database migration testing by
* creating the database with an old version number, then re-open it
* with the current version and seeing a migration happen. To test
* more authentic migrations when things get more complex, we will
* probably want to persist JSON blobs to disk of actual older versions
* and then pass that in to populate the database.
* }
* @key[nukeDb #:optional Boolean]{
* Compel ourselves to nuke the previous database state and start from
* scratch. This only has an effect when IndexedDB has fired an
* onupgradeneeded event.
* }
* ]]
* ]
*/
function MailDB(testOptions, successCb, errorCb, upgradeCb) {
this._db = null;
this._lazyConfigCarryover = null;
/**
* Fatal error handler. This gets to be the error handler for all unexpected
* error cases.
*/
this._fatalError = function(event) {
function explainSource(source) {
if (!source)
return 'unknown source';
if (source instanceof IDBObjectStore)
return 'object store "' + source.name + '"';
if (source instanceof IDBIndex)
return 'index "' + source.name + '" on object store "' +
source.objectStore.name + '"';
if (source instanceof IDBCursor)
return 'cursor on ' + explainSource(source.source);
return 'unexpected source';
}
var explainedSource, target = event.target;
if (target instanceof IDBTransaction) {
explainedSource = 'transaction (' + target.mode + ')';
}
else if (target instanceof IDBRequest) {
explainedSource = 'request as part of ' +
(target.transaction ? target.transaction.mode : 'NO') +
' transaction on ' + explainSource(target.source);
}
else { // dunno, ask it to stringify itself.
explainedSource = target.toString();
}
console.error('indexedDB error:', target.error.name, 'from',
explainedSource);
};
var dbVersion = CUR_VERSION;
if (testOptions && testOptions.dbDelta)
dbVersion += testOptions.dbDelta;
if (testOptions && testOptions.dbVersion)
dbVersion = testOptions.dbVersion;
var openRequest = IndexedDB.open('b2g-email', dbVersion), self = this;
openRequest.onsuccess = function(event) {
self._db = openRequest.result;
successCb();
};
openRequest.onupgradeneeded = function(event) {
console.log('MailDB in onupgradeneeded');
var db = openRequest.result;
// - reset to clean slate
if ((event.oldVersion < FRIENDLY_LAZY_DB_UPGRADE_VERSION) ||
(testOptions && testOptions.nukeDb)) {
self._nukeDB(db);
}
// - friendly, lazy upgrade
else {
var trans = openRequest.transaction;
// Load the current config, save it off so getConfig can use it, then nuke
// like usual. This is obviously a potentially data-lossy approach to
// things; but this is a 'lazy' / best-effort approach to make us more
// willing to bump revs during development, not the holy grail.
self.getConfig(function(configObj, accountInfos) {
if (configObj)
self._lazyConfigCarryover = {
oldVersion: event.oldVersion,
config: configObj,
accountInfos: accountInfos
};
self._nukeDB(db);
}, trans);
}
};
openRequest.onerror = this._fatalError;
}
MailDB.prototype = {
/**
* Reset the contents of the database.
*/
_nukeDB: function(db) {
var existingNames = db.objectStoreNames;
for (var i = 0; i < existingNames.length; i++) {
db.deleteObjectStore(existingNames[i]);
}
db.createObjectStore(TBL_CONFIG);
db.createObjectStore(TBL_FOLDER_INFO);
db.createObjectStore(TBL_HEADER_BLOCKS);
db.createObjectStore(TBL_BODY_BLOCKS);
},
close: function() {
if (this._db) {
this._db.close();
this._db = null;
}
},
getConfig: function(callback, trans) {
var transaction = trans ||
this._db.transaction([TBL_CONFIG, TBL_FOLDER_INFO],
'readonly');
var configStore = transaction.objectStore(TBL_CONFIG),
folderInfoStore = transaction.objectStore(TBL_FOLDER_INFO);
// these will fire sequentially
var configReq = configStore.mozGetAll(),
folderInfoReq = folderInfoStore.mozGetAll();
configReq.onerror = this._fatalError;
// no need to track success, we can read it off folderInfoReq
folderInfoReq.onerror = this._fatalError;
var self = this;
folderInfoReq.onsuccess = function(event) {
var configObj = null, accounts = [], i, obj;
// - Check for lazy carryover.
// IndexedDB provides us with a strong ordering guarantee that this is
// happening after any upgrade check. Doing it outside this closure would
// be race-prone/reliably fail.
if (self._lazyConfigCarryover) {
var lazyCarryover = self._lazyConfigCarryover;
self._lazyConfigCarryover = null;
callback(configObj, accounts, lazyCarryover);
return;
}
// - Process the results
for (i = 0; i < configReq.result.length; i++) {
obj = configReq.result[i];
if (obj.id === 'config')
configObj = obj;
else
accounts.push({def: obj, folderInfo: null});
}
for (i = 0; i < folderInfoReq.result.length; i++) {
accounts[i].folderInfo = folderInfoReq.result[i];
}
try {
callback(configObj, accounts);
}
catch(ex) {
console.error('Problem in configCallback', ex, '\n', ex.stack);
}
};
},
saveConfig: function(config) {
var req = this._db.transaction(TBL_CONFIG, 'readwrite')
.objectStore(TBL_CONFIG)
.put(config, 'config');
req.onerror = this._fatalError;
},
/**
* Save the addition of a new account or when changing account settings. Only
* pass `folderInfo` for the new account case; omit it for changing settings
* so it doesn't get updated. For coherency reasons it should only be updated
* using saveAccountFolderStates.
*/
saveAccountDef: function(config, accountDef, folderInfo, callback) {
var trans = this._db.transaction([TBL_CONFIG, TBL_FOLDER_INFO],
'readwrite');
var configStore = trans.objectStore(TBL_CONFIG);
configStore.put(config, 'config');
configStore.put(accountDef, CONFIG_KEYPREFIX_ACCOUNT_DEF + accountDef.id);
if (folderInfo) {
trans.objectStore(TBL_FOLDER_INFO)
.put(folderInfo, accountDef.id);
}
trans.onerror = this._fatalError;
if (callback) {
trans.oncomplete = function() {
callback();
};
}
},
loadHeaderBlock: function(folderId, blockId, callback) {
var req = this._db.transaction(TBL_HEADER_BLOCKS, 'readonly')
.objectStore(TBL_HEADER_BLOCKS)
.get(folderId + ':' + blockId);
req.onerror = this._fatalError;
req.onsuccess = function() {
callback(req.result);
};
},
loadBodyBlock: function(folderId, blockId, callback) {
var req = this._db.transaction(TBL_BODY_BLOCKS, 'readonly')
.objectStore(TBL_BODY_BLOCKS)
.get(folderId + ':' + blockId);
req.onerror = this._fatalError;
req.onsuccess = function() {
callback(req.result);
};
},
/**
* Coherently update the state of the folderInfo for an account plus all dirty
* blocks at once in a single (IndexedDB and SQLite) commit. If we broke
* folderInfo out into separate keys, we could do this on a per-folder basis
* instead of per-account. Revisit if performance data shows stupidity.
*
* @args[
* @param[accountId]
* @param[folderInfo]
* @param[perFolderStuff @listof[@dict[
* @key[id FolderId]
* @key[headerBlocks @dictof[@key[BlockId] @value[HeaderBlock]]]
* @key[bodyBlocks @dictof[@key[BlockID] @value[BodyBlock]]]
* ]]]
* ]
*/
saveAccountFolderStates: function(accountId, folderInfo, perFolderStuff,
deletedFolderIds, callback) {
var trans = this._db.transaction([TBL_FOLDER_INFO, TBL_HEADER_BLOCKS,
TBL_BODY_BLOCKS], 'readwrite');
trans.onerror = this._fatalError;
trans.objectStore(TBL_FOLDER_INFO).put(folderInfo, accountId);
var headerStore = trans.objectStore(TBL_HEADER_BLOCKS),
bodyStore = trans.objectStore(TBL_BODY_BLOCKS),
i;
/**
* Calling put/delete on operations can be fairly expensive for these blocks
* (4-10ms+) which can cause major jerk while scrolling to we send block
* operations individually (but inside of a single block) to improve
* responsiveness at the cost of throughput.
*/
var operationQueue = [];
function addToQueue() {
var args = Array.slice(arguments);
var store = args.shift();
var type = args.shift();
operationQueue.push({
store: store,
type: type,
args: args
});
}
function workQueue() {
var pendingRequest = operationQueue.shift();
// no more the transition complete handles the callback
if (!pendingRequest)
return;
var store = pendingRequest.store;
var type = pendingRequest.type;
var request = store[type].apply(store, pendingRequest.args);
request.onsuccess = request.onerror = workQueue;
}
for (i = 0; i < perFolderStuff.length; i++) {
var pfs = perFolderStuff[i], block;
for (var headerBlockId in pfs.headerBlocks) {
block = pfs.headerBlocks[headerBlockId];
if (block)
addToQueue(headerStore, 'put', block, pfs.id + ':' + headerBlockId);
else
addToQueue(headerStore, 'delete', pfs.id + ':' + headerBlockId);
}
for (var bodyBlockId in pfs.bodyBlocks) {
block = pfs.bodyBlocks[bodyBlockId];
if (block)
addToQueue(bodyStore, 'put', block, pfs.id + ':' + bodyBlockId);
else
addToQueue(bodyStore, 'delete', pfs.id + ':' + bodyBlockId);
}
}
if (deletedFolderIds) {
for (i = 0; i < deletedFolderIds.length; i++) {
var folderId = deletedFolderIds[i],
range = IDBKeyRange.bound(folderId + ':',
folderId + ':\ufff0',
false, false);
addToQueue(headerStore, 'delete', range);
addToQueue(bodyStore, 'delete', range);
}
}
if (callback) {
trans.addEventListener('complete', function() {
callback();
});
}
workQueue();
return trans;
},
/**
* Delete all traces of an account from the database.
*/
deleteAccount: function(accountId) {
var trans = this._db.transaction([TBL_CONFIG, TBL_FOLDER_INFO,
TBL_HEADER_BLOCKS, TBL_BODY_BLOCKS],
'readwrite');
trans.onerror = this._fatalError;
trans.objectStore(TBL_CONFIG).delete('accountDef:' + accountId);
trans.objectStore(TBL_FOLDER_INFO).delete(accountId);
var range = IDBKeyRange.bound(accountId + '/',
accountId + '/\ufff0',
false, false);
trans.objectStore(TBL_HEADER_BLOCKS).delete(range);
trans.objectStore(TBL_BODY_BLOCKS).delete(range);
},
};
return self;
});

View file

@ -0,0 +1,68 @@
define(function() {
'use strict';
var listeners = {};
var modules = [];
var worker = null;
function register(module) {
var action,
name = module.name;
modules.push(module);
if (module.process) {
action = function(msg) {
module.process(msg.uid, msg.cmd, msg.args);
};
} else if (module.dispatch) {
action = function(msg) {
if (module.dispatch[msg.cmd]) {
module.dispatch[msg.cmd].apply(module.dispatch, msg.args);
}
};
}
listeners[name] = action;
module.sendMessage = function(uid, cmd, args, transferArgs) {
//dump('\x1b[34mM => w: send: ' + name + ' ' + uid + ' ' + cmd + '\x1b[0m\n');
//debug('onmessage: ' + name + ": " + uid + " - " + cmd);
worker.postMessage({
type: name,
uid: uid,
cmd: cmd,
args: args
}, transferArgs);
};
}
function unregister(module) {
delete listeners['on' + module.name];
}
function shutdown() {
modules.forEach(function(module) {
if (module.shutdown)
module.shutdown();
});
}
function useWorker(_worker) {
worker = _worker;
worker.onmessage = function dispatchToListener(evt) {
var data = evt.data;
//dump('\x1b[37mM <= w: recv: '+data.type+' '+data.uid+' '+data.cmd+'\x1b[0m\n');
var listener = listeners[data.type];
if (listener)
listener(data);
};
}
return {
register: register,
unregister: unregister,
useWorker: useWorker,
shutdown: shutdown
};
}); // end define

View file

@ -0,0 +1,266 @@
/**
* The main-thread counterpart to our node-net.js wrapper.
*
* Provides the smarts for streaming the content of blobs. An alternate
* implementation would be to provide a decorating proxy to implement this
* since smart Blob transmission is on the W3C raw-socket hit-list (see
* http://www.w3.org/2012/sysapps/raw-sockets/), but we're already acting like
* node.js's net implementation on the other side of the equation and a totally
* realistic implementation is more work and complexity than our needs require.
*
* Important implementation notes that affect us:
*
* - mozTCPSocket generates ondrain notifications when the send buffer is
* completely empty, not when when we go below the target buffer level.
*
* - bufferedAmount in the child process mozTCPSocket implementation only gets
* updated when the parent process relays a messages to the child process.
* When we are performing bulks sends, this means we will only see
* bufferedAmount go down when we receive an 'ondrain' notification and the
* buffer has hit zero. As such, trying to do anything clever involving
* bufferedAmount other than seeing if it's at zero is not going to do
* anything useful.
*
* Leading to our strategy:
*
* - Always have a pre-fetched block of disk I/O to hand to the socket when we
* get a drain event so that disk I/O does not stall our pipeline.
* (Obviously, if the network is faster than our disk, there is very little
* we can do.)
*
* - Pick a page-size so that in the case where the network is extremely fast
* we are able to maintain good throughput even when our IPC overhead
* dominates. We just pick one page-size; we intentionally avoid any
* excessively clever buffering regimes because those could back-fire and
* such effort is better spent on enhancing TCPSocket.
*/
define(function(require) {
'use strict';
var asyncFetchBlobAsUint8Array =
require('../async_blob_fetcher').asyncFetchBlobAsUint8Array;
// Active sockets
var sockInfoByUID = {};
function open(uid, host, port, options) {
var socket = navigator.mozTCPSocket;
var sock = socket.open(host, port, options);
var sockInfo = sockInfoByUID[uid] = {
uid: uid,
sock: sock,
// Are we in the process of sending a blob? The blob if so.
activeBlob: null,
// Current offset into the blob, if any
blobOffset: 0,
queuedData: null,
// Queued write() calls that are ordering dependent on the Blob being
// fully sent first.
backlog: [],
};
sock.onopen = function(evt) {
self.sendMessage(uid, 'onopen');
};
sock.onerror = function(evt) {
var err = evt.data;
var wrappedErr;
if (err && typeof(err) === 'object') {
wrappedErr = {
name: err.name,
type: err.type,
message: err.message
};
}
else {
wrappedErr = err;
}
self.sendMessage(uid, 'onerror', wrappedErr);
};
sock.ondata = function(evt) {
var buf = evt.data;
self.sendMessage(uid, 'ondata', buf, [buf]);
};
sock.ondrain = function(evt) {
// If we have an activeBlob and data already to send, then send it.
// If we have an activeBlob but no data, then fetchNextBlobChunk has
// an outstanding chunk fetch and it will issue the write directly.
if (sockInfo.activeBlob && sockInfo.queuedData) {
console.log('net-main(' + sockInfo.uid + '): Socket drained, sending.');
sock.send(sockInfo.queuedData.buffer, 0, sockInfo.queuedData.byteLength);
sockInfo.queuedData = null;
// fetch the next chunk or close out the blob; this method does both
fetchNextBlobChunk(sockInfo);
} else {
// Only forward the drain event if we aren't still taking over.
self.sendMessage(uid, 'ondrain');
}
};
sock.onclose = function(evt) {
self.sendMessage(uid, 'onclose');
delete sockInfoByUID[uid];
};
}
function beginBlobSend(sockInfo, blob) {
console.log('net-main(' + sockInfo.uid + '): Blob send of', blob.size,
'bytes');
sockInfo.activeBlob = blob;
sockInfo.blobOffset = 0;
sockInfo.queuedData = null;
fetchNextBlobChunk(sockInfo);
}
/**
* Fetch the next portion of the Blob we are currently sending. Once the read
* completes we will either send the data immediately if the socket's buffer is
* empty or queue it up for sending once the buffer does drain.
*
* This logic is used both in the starting case and to help us reach a steady
* state where (ideally) we always have a pre-fetched buffer of data ready for
* when we hear the next drain event.
*
* We are also responsible for noticing that we're all done sending the Blob.
*/
function fetchNextBlobChunk(sockInfo) {
// We are all done if the next fetch would be beyond the end of the blob
if (sockInfo.blobOffset >= sockInfo.activeBlob.size) {
console.log('net-main(' + sockInfo.uid + '): Blob send completed.',
'backlog length:', sockInfo.backlog.length);
sockInfo.activeBlob = null;
// Drain as much of the backlog as possible.
var backlog = sockInfo.backlog;
while (backlog.length) {
var sendArgs = backlog.shift();
var data = sendArgs[0];
if (data instanceof Blob) {
beginBlobSend(sockInfo, data);
return;
}
sockInfo.sock.send(data, sendArgs[1], sendArgs[2]);
}
// (the backlog is now empty)
return;
}
var nextOffset =
Math.min(sockInfo.blobOffset + self.BLOB_BLOCK_READ_SIZE,
sockInfo.activeBlob.size);
console.log('net-main(' + sockInfo.uid + '): Fetching bytes',
sockInfo.blobOffset, 'through', nextOffset, 'of',
sockInfo.activeBlob.size);
var blobSlice = sockInfo.activeBlob.slice(
sockInfo.blobOffset,
nextOffset);
sockInfo.blobOffset = nextOffset;
function gotChunk(err, binaryDataU8) {
console.log('net-main(' + sockInfo.uid + '): Retrieved chunk');
if (err) {
// I/O errors are fatal to the connection; our abstraction does not let us
// bubble the error. The good news is that errors are highly unlikely.
sockInfo.sock.close();
return;
}
// If the socket has already drained its buffer, then just send the data
// right away and re-schedule ourselves.
if (sockInfo.sock.bufferedAmount === 0) {
console.log('net-main(' + sockInfo.uid + '): Sending chunk immediately.');
sockInfo.sock.send(binaryDataU8.buffer, 0, binaryDataU8.byteLength);
fetchNextBlobChunk(sockInfo);
return;
}
sockInfo.queuedData = binaryDataU8;
};
asyncFetchBlobAsUint8Array(blobSlice, gotChunk);
}
function close(uid) {
var sockInfo = sockInfoByUID[uid];
if (!sockInfo)
return;
var sock = sockInfo.sock;
sock.close();
sock.onopen = null;
sock.onerror = null;
sock.ondata = null;
sock.ondrain = null;
sock.onclose = null;
self.sendMessage(uid, 'onclose');
delete sockInfoByUID[uid];
}
function write(uid, data, offset, length) {
var sockInfo = sockInfoByUID[uid];
if (!sockInfo) {
return;
}
// If there is an activeBlob, then the write must be queued or we would end up
// mixing this write in with our Blob and that would be embarassing.
if (sockInfo.activeBlob) {
sockInfo.backlog.push([data, offset, length]);
return;
}
// Fake an onprogress event so that we can delay wakelock expiration
// as long as data still flows to the server.
self.sendMessage(uid, 'onprogress', []);
if (data instanceof Blob) {
beginBlobSend(sockInfo, data);
}
else {
sockInfo.sock.send(data, offset, length);
}
}
function upgradeToSecure(uid) {
var sockInfo = sockInfoByUID[uid];
if (!sockInfo)
return;
sockInfo.sock.upgradeToSecure();
}
var self = {
name: 'netsocket',
sendMessage: null,
/**
* What size bites (in bytes) should we take of the Blob for streaming
* purposes? See the file header for the sizing rationale.
*/
BLOB_BLOCK_READ_SIZE: 96 * 1024,
process: function(uid, cmd, args) {
switch (cmd) {
case 'open':
open(uid, args[0], args[1], args[2]);
break;
case 'close':
close(uid);
break;
case 'write':
write(uid, args[0], args[1], args[2]);
break;
case 'upgradeToSecure':
upgradeToSecure(uid);
break;
default:
console.error('Unhandled net-main command:', cmd);
break;
}
}
};
return self;
});

View file

@ -0,0 +1,11 @@
(function () {
// Like setTimeout, but only takes a function argument. There's
// no time argument (always zero) and no arguments (you have to
// use a closure).
function setZeroTimeout(fn) {
setTimeout(fn);
}
// Add the one thing we want added to the window object.
window.setZeroTimeout = setZeroTimeout;
}());

View file

@ -0,0 +1,206 @@
/**
* Assist mailapi/testhelper by spinning up the main thread support modules and
* establishing a bouncer to redirect all mailapi traffic back to a MailAPI
* instance instantiated in the worker.
**/
define(
[
'./main-router',
'./configparser-main',
'./cronsync-main',
'./devicestorage-main',
'./maildb-main',
'./net-main',
'../mailapi',
'exports'
],
function(
$router,
$configparser,
$cronsync,
$devicestorage,
$maildb,
$net,
$mailapi,
exports
) {
var realisticBridge = {
name: 'bridge',
sendMessage: null,
process: function(uid, cmd, args) {
bouncedBridge.sendMessage(uid, cmd, args);
}
};
var bouncedBridge = {
name: 'bounced-bridge',
sendMessage: null,
process: function(uid, cmd, args) {
realisticBridge.sendMessage(uid, cmd, args);
}
};
$router.register(realisticBridge);
$router.register(bouncedBridge);
var gMailAPI = null;
var testHelper = {
name: 'testhelper',
sendMessage: null,
process: function(uid, cmd, args) {
if (cmd === 'create-mailapi') {
gMailAPI = new $mailapi.MailAPI();
gMailAPI.__bridgeSend = function(msg) {
mainBridge.sendMessage(null, null, msg);
};
}
/**
* Support testAccount.getMessageBodyOnMainThread
*/
else if (cmd === 'runWithBody') {
try {
var func;
eval('func = ' + args.func + ';');
gMailAPI._getBodyForMessage(
{ id: args.headerId, date: args.headerDate },
null,
function(body) {
console.log('got body, invoking func!');
try {
func(args.arg, body, function(results) {
testHelper.sendMessage(uid, cmd, [results]);
body.die();
});
}
catch (ex) {
console.error('problem in runWithBody func', ex, '\n', ex.stack);
}
});
} catch (ex) {
console.error('problem with runWithBody', ex, '\n', ex.stack);
}
}
/**
* Support testUniverse.help_checkDatabaseDoesNotContain
*/
else if (cmd === 'checkDatabaseDoesNotContain') {
var tablesAndKeyPrefixes = args;
var idb = $maildb._debugDB._db,
desiredStores = [], i, checkArgs;
for (i = 0; i < tablesAndKeyPrefixes.length; i++) {
checkArgs = tablesAndKeyPrefixes[i];
desiredStores.push(checkArgs.table);
}
var trans = idb.transaction(desiredStores, 'readonly');
var results = [];
var sendResults = function() {
testHelper.sendMessage(uid, 'checkDatabaseDoesNotContain', [results]);
};
var waitCount = tablesAndKeyPrefixes.length;
tablesAndKeyPrefixes.forEach(function(checkArgs) {
var store = trans.objectStore(checkArgs.table),
range = IDBKeyRange.bound(checkArgs.prefix,
checkArgs.prefix + '\ufff0',
false, false),
req = store.get(range);
req.onerror = function(event) {
results.push({ errCode: event.target.errorCode });
if (--waitCount === 0)
sendResults();
};
req.onsuccess = function() {
results.push({ errCode: null,
table: checkArgs.table,
prefix: checkArgs.prefix,
hasResult: req.result !== undefined });
if (--waitCount === 0)
sendResults();
};
});
}
/**
* Support fake-server stand-up.
*/
else if (cmd === 'startFakeServer') {
}
}
};
$router.register(testHelper);
// Bridge connection for a MailAPI instance on the main thread
var mainBridge = {
name: 'main-bridge',
sendMessage: null,
process: function(uid, cmd, args) {
if (gMailAPI)
gMailAPI.__bridgeReceive(args);
}
};
$router.register(mainBridge);
var deviceStorageTestHelper = {
name: 'th_devicestorage',
sendMessage: null,
process: function(uid, cmd, args) {
if (cmd === 'attach') {
this._storage = navigator.getDeviceStorage(args);
this._storage.addEventListener('change', this._bound_onChange);
this.sendMessage(null, 'attached', null);
}
else if (cmd === 'detach') {
var detachCount = 1, self = this;
function nextDetach(path, result) {
if (path) {
console.log('devicestorage:', path, result);
}
if (--detachCount > 0) {
return;
}
console.log('devicestorage: detach cleanup completed');
self._storage.removeEventListener('change', self._bound_onChange);
self._storage = null;
self.sendMessage(null, 'detached', null);
}
args.nuke.forEach(function(path) {
detachCount++;
var req = self._storage.delete(path);
// the deletion notifications will be generated as a side-effect
req.onsuccess = nextDetach.bind(null, path, 'success');
req.onerror = nextDetach.bind(null, path, 'error');
});
nextDetach();
}
else if (cmd === 'get') {
var req = this._storage.get(args.path);
req.onsuccess = function() {
this.sendMessage(null, 'got',
{ id: args.id, error: null, blob: req.result });
}.bind(this);
req.onerror = function() {
this.sendMessage(null, 'got',
{ id: args.id, error: '' + req.error, blob: null });
}.bind(this);
}
},
_storage: null,
_bound_onChange: null,
_onChange: function(event) {
this.sendMessage(null, 'change', { reason: event.reason, path: event.path });
}
};
deviceStorageTestHelper._bound_onChange =
deviceStorageTestHelper._onChange.bind(deviceStorageTestHelper);
$router.register(deviceStorageTestHelper);
$router.register($configparser);
$router.register($cronsync);
$router.register($devicestorage);
$router.register($maildb);
$router.register($net);
}); // end define

View file

@ -0,0 +1,46 @@
/**
* The docs for this can be found in `mailapi/wakelocks.js`.
*
* This file runs on the main thread, receiving messages sent from a
* SmartWakeLock instance -> through the router -> to this file.
*/
define(function() {
'use strict';
function debug(str) {
dump('WakeLocks: ' + str + '\n');
}
var nextId = 1;
var locks = {};
function requestWakeLock(type) {
var lock = navigator.requestWakeLock(type);
var id = nextId++;
locks[id] = lock;
return id;
}
var self = {
name: 'wakelocks',
sendMessage: null,
process: function(uid, cmd, args) {
debug('process ' + cmd + ' ' + JSON.stringify(args));
switch (cmd) {
case 'requestWakeLock':
var type = args[0];
self.sendMessage(uid, cmd, [requestWakeLock(type)]);
break;
case 'unlock':
var id = args[0];
var lock = locks[id];
if (lock) {
lock.unlock();
delete locks[id];
}
self.sendMessage(uid, cmd, []);
}
}
};
return self;
});