VulpesOS/gaia/apps/email/js/ext/mailbridge.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

1360 lines
42 KiB
JavaScript
Executable file

/**
*
**/
define(
[
'logic',
'./util',
'./mailchew-strings',
'./date',
'./slice_bridge_proxy',
'require',
'module',
'exports'
],
function(
logic,
$imaputil,
$mailchewStrings,
$date,
$sliceBridgeProxy,
require,
$module,
exports
) {
var bsearchForInsert = $imaputil.bsearchForInsert,
bsearchMaybeExists = $imaputil.bsearchMaybeExists,
SliceBridgeProxy = $sliceBridgeProxy.SliceBridgeProxy;
function toBridgeWireOn(x) {
return x.toBridgeWire();
}
var FOLDER_TYPE_TO_SORT_PRIORITY = {
account: 'a',
inbox: 'c',
starred: 'e',
important: 'f',
drafts: 'g',
localdrafts: 'h',
outbox: 'i',
queue: 'j',
sent: 'k',
junk: 'l',
trash: 'n',
archive: 'p',
normal: 'z',
// nomail folders are annoying since they are basically just hierarchy,
// but they are also rare and should only happen amongst normal folders.
nomail: 'z',
};
/**
* Make a folder sorting function that groups folders by account, puts the
* account header first in that group, maps priorities using
* FOLDER_TYPE_TO_SORT_PRIORITY, then sorts by path within that.
*
* This is largely necessitated by localeCompare being at the mercy of glibc's
* locale database and failure to fallback to unicode code points for
* comparison purposes.
*/
function makeFolderSortString(account, folder) {
if (!folder)
return account.id;
var parentFolder = account.getFolderMetaForFolderId(folder.parentId);
return makeFolderSortString(account, parentFolder) + '!' +
FOLDER_TYPE_TO_SORT_PRIORITY[folder.type] + '!' +
folder.name.toLocaleLowerCase();
}
function strcmp(a, b) {
if (a < b)
return -1;
else if (a > b)
return 1;
return 0;
}
function checkIfAddressListContainsAddress(list, addrPair) {
if (!list)
return false;
var checkAddress = addrPair.address;
for (var i = 0; i < list.length; i++) {
if (list[i].address === checkAddress)
return true;
}
return false;
}
/**
* There is exactly one `MailBridge` instance for each `MailAPI` instance.
* `same-frame-setup.js` is the only place that hooks them up together right
* now.
*/
function MailBridge(universe, name) {
this.universe = universe;
this.universe.registerBridge(this);
logic.defineScope(this, 'MailBridge', { name: name });
/** @dictof[@key[handle] @value[BridgedViewSlice]]{ live slices } */
this._slices = {};
/** @dictof[@key[namespace] @value[@listof[BridgedViewSlice]]] */
this._slicesByType = {
accounts: [],
identities: [],
folders: [],
headers: [],
matchedHeaders: [],
};
/**
* Observed bodies in the format of:
*
* @dictof[
* @key[suid]
* @value[@dictof[
* @key[handleId]
* @value[@oneof[Function null]]
* ]]
* ]
*
* Similar in concept to folder slices but specific to bodies.
*/
this._observedBodies = {};
// outstanding persistent objects that aren't slices. covers: composition
this._pendingRequests = {};
//
this._lastUndoableOpPair = null;
}
exports.MailBridge = MailBridge;
MailBridge.prototype = {
__sendMessage: function(msg) {
throw new Error('This is supposed to get hidden by an instance var.');
},
__receiveMessage: function mb___receiveMessage(msg) {
var implCmdName = '_cmd_' + msg.type;
if (!(implCmdName in this)) {
logic(this, 'badMessageType', { type: msg.type });
return;
}
logic(this, 'cmd', {
type: msg.type,
msg: msg
});
try {
this[implCmdName](msg);
} catch(ex) {
logic.fail(ex);
return; // note that we did not throw
}
},
_cmd_ping: function mb__cmd_ping(msg) {
this.__sendMessage({
type: 'pong',
handle: msg.handle,
});
},
_cmd_modifyConfig: function mb__cmd_modifyConfig(msg) {
this.universe.modifyConfig(msg.mods);
},
/**
* Public api to verify if body has observers.
*
*
* MailBridge.bodyHasObservers(header.id) // => true/false.
*
*/
bodyHasObservers: function(suid) {
return !!this._observedBodies[suid];
},
notifyConfig: function(config) {
this.__sendMessage({
type: 'config',
config: config,
});
},
_cmd_debugSupport: function mb__cmd_debugSupport(msg) {
switch (msg.cmd) {
case 'setLogging':
this.universe.modifyConfig({ debugLogging: msg.arg });
break;
case 'dumpLog':
switch (msg.arg) {
case 'storage':
this.universe.dumpLogToDeviceStorage();
break;
}
break;
}
},
_cmd_setInteractive: function mb__cmd_setInteractive(msg) {
this.universe.setInteractive();
},
_cmd_localizedStrings: function mb__cmd_localizedStrings(msg) {
$mailchewStrings.set(msg.strings);
},
_cmd_learnAboutAccount: function(msg) {
this.universe.learnAboutAccount(msg.details).then(
function success(info) {
this.__sendMessage({
type: 'learnAboutAccountResults',
handle: msg.handle,
data: info
});
}.bind(this),
function errback(err) {
this.__sendMessage({
type: 'learnAboutAccountResults',
handle: msg.handle,
data: { result: 'no-config-info', configInfo: null }
});
}.bind(this));
},
_cmd_tryToCreateAccount: function mb__cmd_tryToCreateAccount(msg) {
var self = this;
this.universe.tryToCreateAccount(msg.details, msg.domainInfo,
function(error, account, errorDetails) {
self.__sendMessage({
type: 'tryToCreateAccountResults',
handle: msg.handle,
account: account ? account.toBridgeWire() : null,
error: error,
errorDetails: errorDetails,
});
});
},
_cmd_clearAccountProblems: function mb__cmd_clearAccountProblems(msg) {
var account = this.universe.getAccountForAccountId(msg.accountId),
self = this;
account.checkAccount(function(incomingErr, outgoingErr) {
// Note that ActiveSync accounts won't have an outgoingError,
// but that's fine. It just means that outgoing never errors!
function canIgnoreError(err) {
// If we succeeded or the problem was not an authentication,
// assume everything went fine. This includes the case we're
// offline.
return (!err || (
err !== 'bad-user-or-pass' &&
err !== 'bad-address' &&
err !== 'needs-oauth-reauth' &&
err !== 'imap-disabled'
));
}
if (canIgnoreError(incomingErr) && canIgnoreError(outgoingErr)) {
self.universe.clearAccountProblems(account);
}
self.__sendMessage({
type: 'clearAccountProblems',
handle: msg.handle,
});
});
},
_cmd_modifyAccount: function mb__cmd_modifyAccount(msg) {
var account = this.universe.getAccountForAccountId(msg.accountId),
accountDef = account.accountDef;
for (var key in msg.mods) {
var val = msg.mods[key];
switch (key) {
case 'name':
accountDef.name = val;
break;
case 'username':
// See the 'password' section below and/or
// MailAPI.modifyAccount docs for the rationale for this
// username equality check:
if (accountDef.credentials.outgoingUsername ===
accountDef.credentials.username) {
accountDef.credentials.outgoingUsername = val;
}
accountDef.credentials.username = val;
break;
case 'incomingUsername':
accountDef.credentials.username = val;
break;
case 'outgoingUsername':
accountDef.credentials.outgoingUsername = val;
break;
case 'password':
// 'password' is for changing both passwords, if they
// currently match. If this account contains an SMTP
// password (only composite ones will) and the passwords
// were previously the same, assume that they both need to
// remain the same. NOTE: By doing this, we save the user
// from typing their password twice in the extremely common
// case that both passwords are actually the same. If the
// SMTP password is actually different, we'll just prompt
// them for that independently if we discover it's still not
// correct.
if (accountDef.credentials.outgoingPassword ===
accountDef.credentials.password) {
accountDef.credentials.outgoingPassword = val;
}
accountDef.credentials.password = val;
break;
case 'incomingPassword':
accountDef.credentials.password = val;
break;
case 'outgoingPassword':
accountDef.credentials.outgoingPassword = val;
break;
case 'oauthTokens':
var oauth2 = accountDef.credentials.oauth2;
oauth2.accessToken = val.accessToken;
oauth2.refreshToken = val.refreshToken;
oauth2.expireTimeMS = val.expireTimeMS;
break;
case 'identities':
// TODO: support identity mutation
// we expect a list of identity mutation objects, namely an id and the
// rest are attributes to change
break;
case 'servers':
// TODO: support server mutation
// we expect a list of server mutation objects; namely, the type names
// the server and the rest are attributes to change
break;
case 'syncRange':
accountDef.syncRange = val;
break;
case 'syncInterval':
accountDef.syncInterval = val;
break;
case 'notifyOnNew':
accountDef.notifyOnNew = val;
break;
case 'playSoundOnSend':
accountDef.playSoundOnSend = val;
break;
case 'setAsDefault':
// Weird things can happen if the device's clock goes back in time,
// but this way, at least the user can change their default if they
// cycle through their accounts.
if (val)
accountDef.defaultPriority = $date.NOW();
break;
default:
throw new Error('Invalid key for modifyAccount: "' + key + '"');
}
}
this.universe.saveAccountDef(accountDef, null);
this.__sendMessage({
type: 'modifyAccount',
handle: msg.handle,
});
},
_cmd_deleteAccount: function mb__cmd_deleteAccount(msg) {
this.universe.deleteAccount(msg.accountId);
},
_cmd_modifyIdentity: function mb__cmd_modifyIdentity(msg) {
var account = this.universe.getAccountForSenderIdentityId(msg.identityId),
accountDef = account.accountDef,
identity = this.universe.getIdentityForSenderIdentityId(msg.identityId);
for (var key in msg.mods) {
var val = msg.mods[key];
switch (key) {
case 'name':
identity.name = val;
break;
case 'address':
identity.address = val;
break;
case 'replyTo':
identity.replyTo = val;
break;
case 'signature':
identity.signature = val;
break;
case 'signatureEnabled':
identity.signatureEnabled = val;
break;
default:
throw new Error('Invalid key for modifyIdentity: "' + key + '"');
}
}
// accountDef has the identity, so this persists it as well
this.universe.saveAccountDef(accountDef, null, function() {
this.__sendMessage({
type: 'modifyIdentity',
handle: msg.handle,
});
}.bind(this));
},
/**
* Notify the frontend that login failed.
*
* @param account
* @param {string} problem
* @param {'incoming'|'outgoing'} whichSide
*/
notifyBadLogin: function mb_notifyBadLogin(account, problem, whichSide) {
this.__sendMessage({
type: 'badLogin',
account: account.toBridgeWire(),
problem: problem,
whichSide: whichSide,
});
},
_cmd_viewAccounts: function mb__cmd_viewAccounts(msg) {
var proxy = this._slices[msg.handle] =
new SliceBridgeProxy(this, 'accounts', msg.handle);
proxy.markers = this.universe.accounts.map(function(x) { return x.id; });
this._slicesByType['accounts'].push(proxy);
var wireReps = this.universe.accounts.map(toBridgeWireOn);
// send all the accounts in one go.
proxy.sendSplice(0, 0, wireReps, true, false);
},
notifyAccountAdded: function mb_notifyAccountAdded(account) {
var accountWireRep = account.toBridgeWire();
var i, proxy, slices, wireSplice = null, markersSplice = null;
// -- notify account slices
slices = this._slicesByType['accounts'];
for (i = 0; i < slices.length; i++) {
proxy = slices[i];
proxy.sendSplice(proxy.markers.length, 0, [accountWireRep], false, false);
proxy.markers.push(account.id);
}
// -- notify folder slices
accountWireRep = account.toBridgeFolder();
slices = this._slicesByType['folders'];
var startMarker = makeFolderSortString(account, accountWireRep),
idxStart;
for (i = 0; i < slices.length; i++) {
proxy = slices[i];
// If it's filtered to an account, it can't care about us. (You can't
// know about an account before it's created.)
if (proxy.mode === 'account')
continue;
idxStart = bsearchForInsert(proxy.markers, startMarker, strcmp);
wireSplice = [accountWireRep];
markersSplice = [startMarker];
for (var iFolder = 0; iFolder < account.folders.length; iFolder++) {
var folder = account.folders[iFolder],
folderMarker = makeFolderSortString(account, folder),
idxFolder = bsearchForInsert(markersSplice, folderMarker, strcmp);
wireSplice.splice(idxFolder, 0, folder);
markersSplice.splice(idxFolder, 0, folderMarker);
}
proxy.sendSplice(idxStart, 0, wireSplice, false, false);
proxy.markers.splice.apply(proxy.markers,
[idxStart, 0].concat(markersSplice));
}
},
/**
* Generate modifications for an account. We only generate this for account
* queries proper and not the folder representations of accounts because we
* define that there is nothing interesting mutable for the folder
* representations.
*/
notifyAccountModified: function(account) {
var slices = this._slicesByType['accounts'],
accountWireRep = account.toBridgeWire();
for (var i = 0; i < slices.length; i++) {
var proxy = slices[i];
var idx = proxy.markers.indexOf(account.id);
if (idx !== -1) {
proxy.sendUpdate([idx, accountWireRep]);
}
}
},
notifyAccountRemoved: function(accountId) {
var i, proxy, slices;
// -- notify account slices
slices = this._slicesByType['accounts'];
for (i = 0; i < slices.length; i++) {
proxy = slices[i];
var idx = proxy.markers.indexOf(accountId);
if (idx !== -1) {
proxy.sendSplice(idx, 1, [], false, false);
proxy.markers.splice(idx, 1);
}
}
// -- notify folder slices
slices = this._slicesByType['folders'];
var startMarker = accountId + '!!',
endMarker = accountId + '!|';
for (i = 0; i < slices.length; i++) {
proxy = slices[i];
var idxStart = bsearchForInsert(proxy.markers, startMarker,
strcmp),
idxEnd = bsearchForInsert(proxy.markers, endMarker,
strcmp);
if (idxEnd !== idxStart) {
proxy.sendSplice(idxStart, idxEnd - idxStart, [], false, false);
proxy.markers.splice(idxStart, idxEnd - idxStart);
}
}
},
_cmd_viewSenderIdentities: function mb__cmd_viewSenderIdentities(msg) {
var proxy = this._slices[msg.handle] =
new SliceBridgeProxy(this, 'identities', msg.handle);
this._slicesByType['identities'].push(proxy);
var wireReps = this.universe.identities;
// send all the identities in one go.
proxy.sendSplice(0, 0, wireReps, true, false);
},
_cmd_requestBodies: function(msg) {
var self = this;
this.universe.downloadBodies(msg.messages, msg.options, function() {
self.__sendMessage({
type: 'requestBodiesComplete',
handle: msg.handle,
requestId: msg.requestId
});
});
},
notifyFolderAdded: function(account, folderMeta) {
var newMarker = makeFolderSortString(account, folderMeta);
var slices = this._slicesByType['folders'];
for (var i = 0; i < slices.length; i++) {
var proxy = slices[i];
var idx = bsearchForInsert(proxy.markers, newMarker, strcmp);
proxy.sendSplice(idx, 0, [folderMeta], false, false);
proxy.markers.splice(idx, 0, newMarker);
}
},
notifyFolderModified: function(account, folderMeta) {
var marker = makeFolderSortString(account, folderMeta);
var slices = this._slicesByType['folders'];
for (var i = 0; i < slices.length; i++) {
var proxy = slices[i];
var idx = bsearchMaybeExists(proxy.markers, marker, strcmp);
if (idx === null)
continue;
proxy.sendUpdate([idx, folderMeta]);
}
},
notifyFolderRemoved: function(account, folderMeta) {
var marker = makeFolderSortString(account, folderMeta);
var slices = this._slicesByType['folders'];
for (var i = 0; i < slices.length; i++) {
var proxy = slices[i];
var idx = bsearchMaybeExists(proxy.markers, marker, strcmp);
if (idx === null)
continue;
proxy.sendSplice(idx, 1, [], false, false);
proxy.markers.splice(idx, 1);
}
},
/**
* Sends a notification of a change in the body. Because FolderStorage is
* the authoritative store of body representations and access is currently
* mediated through mutexes, this method should really only be called by
* FolderStorage.updateMessageBody.
*
* @param suid {SUID}
* The message whose body representation has been updated
* @param detail {Object}
* See {{#crossLink "FolderStorage/updateMessageBody"}{{/crossLink}} for
* more information on the structure of this object.
* @param body {BodyInfo}
* The current representation of the body.
*/
notifyBodyModified: function(suid, detail, body) {
var handles = this._observedBodies[suid];
var defaultHandler = this.__sendMessage;
if (handles) {
for (var handle in handles) {
// the suid may have an existing handler which captures the output of
// the notification instead of dispatching here... This allows us to
// aggregate pending notifications while fetching the bodies so updates
// never come before the actual body.
var emit = handles[handle] || defaultHandler;
emit.call(this, {
type: 'bodyModified',
handle: handle,
bodyInfo: body,
detail: detail
});
}
}
},
_cmd_viewFolders: function mb__cmd_viewFolders(msg) {
var proxy = this._slices[msg.handle] =
new SliceBridgeProxy(this, 'folders', msg.handle);
this._slicesByType['folders'].push(proxy);
proxy.mode = msg.mode;
proxy.argument = msg.argument;
var markers = proxy.markers = [];
var wireReps = [];
function pushAccountFolders(acct) {
for (var iFolder = 0; iFolder < acct.folders.length; iFolder++) {
var folder = acct.folders[iFolder];
var newMarker = makeFolderSortString(acct, folder);
var idx = bsearchForInsert(markers, newMarker, strcmp);
wireReps.splice(idx, 0, folder);
markers.splice(idx, 0, newMarker);
}
}
if (msg.mode === 'account') {
pushAccountFolders(
this.universe.getAccountForAccountId(msg.argument));
}
else {
var accounts = this.universe.accounts.concat();
// sort accounts by their id's
accounts.sort(function (a, b) {
return a.id.localeCompare(b.id);
});
for (var iAcct = 0; iAcct < accounts.length; iAcct++) {
var acct = accounts[iAcct], acctBridgeRep = acct.toBridgeFolder(),
acctMarker = makeFolderSortString(acct, acctBridgeRep),
idxAcct = bsearchForInsert(markers, acctMarker, strcmp);
wireReps.splice(idxAcct, 0, acctBridgeRep);
markers.splice(idxAcct, 0, acctMarker);
pushAccountFolders(acct);
}
}
proxy.sendSplice(0, 0, wireReps, true, false);
},
_cmd_viewFolderMessages: function mb__cmd_viewFolderMessages(msg) {
var proxy = this._slices[msg.handle] =
new SliceBridgeProxy(this, 'headers', msg.handle);
this._slicesByType['headers'].push(proxy);
var account = this.universe.getAccountForFolderId(msg.folderId);
account.sliceFolderMessages(msg.folderId, proxy);
},
_cmd_searchFolderMessages: function mb__cmd_searchFolderMessages(msg) {
var proxy = this._slices[msg.handle] =
new SliceBridgeProxy(this, 'matchedHeaders', msg.handle);
this._slicesByType['matchedHeaders'].push(proxy);
var account = this.universe.getAccountForFolderId(msg.folderId);
account.searchFolderMessages(
msg.folderId, proxy, msg.phrase, msg.whatToSearch);
},
_cmd_refreshHeaders: function mb__cmd_refreshHeaders(msg) {
var proxy = this._slices[msg.handle];
if (!proxy) {
logic(this, 'badSliceHandle', { handle: msg.handle });
return;
}
if (proxy.__listener)
proxy.__listener.refresh();
},
_cmd_growSlice: function mb__cmd_growSlice(msg) {
var proxy = this._slices[msg.handle];
if (!proxy) {
logic(this, 'badSliceHandle', { handle: msg.handle });
return;
}
if (proxy.__listener)
proxy.__listener.reqGrow(msg.dirMagnitude, msg.userRequestsGrowth);
},
_cmd_shrinkSlice: function mb__cmd_shrinkSlice(msg) {
var proxy = this._slices[msg.handle];
if (!proxy) {
logic(this, 'badSliceHandle', { handle: msg.handle });
return;
}
if (proxy.__listener)
proxy.__listener.reqNoteRanges(
msg.firstIndex, msg.firstSuid, msg.lastIndex, msg.lastSuid);
},
_cmd_killSlice: function mb__cmd_killSlice(msg) {
var proxy = this._slices[msg.handle];
if (!proxy) {
logic(this, 'badSliceHandle', { handle: msg.handle });
return;
}
delete this._slices[msg.handle];
var proxies = this._slicesByType[proxy._ns],
idx = proxies.indexOf(proxy);
proxies.splice(idx, 1);
proxy.die();
this.__sendMessage({
type: 'sliceDead',
handle: msg.handle,
});
},
_cmd_getBody: function mb__cmd_getBody(msg) {
var self = this;
// map the message id to the folder storage
var folderStorage = this.universe.getFolderStorageForMessageSuid(msg.suid);
// when requesting the body we also create a observer to notify the client
// of events... We never want to send the updates before fetching the body
// so we buffer them here with a temporary handler.
var pendingUpdates = [];
var catchPending = function catchPending(msg) {
pendingUpdates.push(msg);
};
if (!this._observedBodies[msg.suid])
this._observedBodies[msg.suid] = {};
this._observedBodies[msg.suid][msg.handle] = catchPending;
var handler = function(bodyInfo) {
self.__sendMessage({
type: 'gotBody',
handle: msg.handle,
bodyInfo: bodyInfo
});
// if all body reps where requested we verify that all are present
// otherwise we begin the request for more body reps.
if (
msg.downloadBodyReps &&
!folderStorage.messageBodyRepsDownloaded(bodyInfo)
) {
self.universe.downloadMessageBodyReps(
msg.suid,
msg.date,
function() { /* we don't care it will send update events */ }
);
}
// dispatch pending updates...
pendingUpdates.forEach(self.__sendMessage, self);
pendingUpdates = null;
// revert to default handler. Note! this is intentionally
// set to null and not deleted if deleted the observer is removed.
self._observedBodies[msg.suid][msg.handle] = null;
};
if (msg.withBodyReps)
folderStorage.getMessageBodyWithReps(msg.suid, msg.date, handler);
else
folderStorage.getMessageBody(msg.suid, msg.date, handler);
},
_cmd_killBody: function(msg) {
var handles = this._observedBodies[msg.id];
if (handles) {
delete handles[msg.handle];
var purgeHandles = true;
for (var key in handles) {
purgeHandles = false;
break;
}
if (purgeHandles) {
delete this._observedBodies[msg.id];
}
}
this.__sendMessage({
type: 'bodyDead',
handle: msg.handle
});
},
_cmd_downloadAttachments: function mb__cmd__downloadAttachments(msg) {
var self = this;
this.universe.downloadMessageAttachments(
msg.suid, msg.date, msg.relPartIndices, msg.attachmentIndices,
msg.registerAttachments,
function(err) {
self.__sendMessage({
type: 'downloadedAttachments',
handle: msg.handle
});
});
},
//////////////////////////////////////////////////////////////////////////////
// Message Mutation
//
// All mutations are told to the universe which breaks the modifications up on
// a per-account basis.
_cmd_modifyMessageTags: function mb__cmd_modifyMessageTags(msg) {
// XXXYYY
// - The mutations are written to the database for persistence (in case
// we fail to make the change in a timely fashion) and so that we can
// know enough to reverse the operation.
// - Speculative changes are made to the headers in the database locally.
var longtermIds = this.universe.modifyMessageTags(
msg.opcode, msg.messages, msg.addTags, msg.removeTags);
this.__sendMessage({
type: 'mutationConfirmed',
handle: msg.handle,
longtermIds: longtermIds,
});
},
_cmd_deleteMessages: function mb__cmd_deleteMessages(msg) {
var longtermIds = this.universe.deleteMessages(
msg.messages);
this.__sendMessage({
type: 'mutationConfirmed',
handle: msg.handle,
longtermIds: longtermIds,
});
},
_cmd_moveMessages: function mb__cmd_moveMessages(msg) {
var longtermIds = this.universe.moveMessages(
msg.messages, msg.targetFolder, function(err, moveMap) {
this.__sendMessage({
type: 'mutationConfirmed',
handle: msg.handle,
longtermIds: longtermIds,
result: moveMap
});
}.bind(this));
},
_cmd_sendOutboxMessages: function(msg) {
var account = this.universe.getAccountForAccountId(msg.accountId);
this.universe.sendOutboxMessages(account, {
reason: 'api request'
}, function(err) {
this.__sendMessage({
type: 'sendOutboxMessages',
handle: msg.handle
});
}.bind(this));
},
_cmd_setOutboxSyncEnabled: function(msg) {
var account = this.universe.getAccountForAccountId(msg.accountId);
this.universe.setOutboxSyncEnabled(
account, msg.outboxSyncEnabled, function() {
this.__sendMessage({
type: 'setOutboxSyncEnabled',
handle: msg.handle
});
}.bind(this));
},
_cmd_undo: function mb__cmd_undo(msg) {
this.universe.undoMutation(msg.longtermIds);
},
//////////////////////////////////////////////////////////////////////////////
// Composition
_cmd_beginCompose: function mb__cmd_beginCompose(msg) {
require(['./drafts/composer', 'mailchew'], function ($composer, $mailchew) {
var req = this._pendingRequests[msg.handle] = {
type: 'compose',
active: 'begin',
account: null,
persistedNamer: null,
die: false
};
// - figure out the identity to use
var account, identity, folderId;
if (msg.mode === 'new' && msg.submode === 'folder')
account = this.universe.getAccountForFolderId(msg.refSuid);
else
account = this.universe.getAccountForMessageSuid(msg.refSuid);
req.account = account;
identity = account.identities[0];
var bodyText = $mailchew.generateBaseComposeBody(identity);
if (msg.mode !== 'reply' && msg.mode !== 'forward') {
return this.__sendMessage({
type: 'composeBegun',
handle: msg.handle,
error: null,
identity: identity,
subject: '',
body: { text: bodyText, html: null },
to: [],
cc: [],
bcc: [],
references: null,
attachments: [],
});
}
var folderStorage =
this.universe.getFolderStorageForMessageSuid(msg.refSuid);
var self = this;
folderStorage.getMessage(
msg.refSuid, msg.refDate, { withBodyReps: true }, function(res) {
if (!res) {
// cannot compose a reply/fwd message without a header/body
return console.warn(
'Cannot compose message missing header/body: ',
msg.refSuid
);
}
var header = res.header;
var bodyInfo = res.body;
if (msg.mode === 'reply') {
var rTo, rCc, rBcc;
// Clobber the sender's e-mail with the reply-to address if provided.
// The Reply-To header can contain multiple addresses, but we only
// reply to the first. In the event of a reply-to address, we clobber
// both the name and address with the provided value; notably, if the
// reply-to header doesn't contain a display name, we leave the
// display name blank as intended. A quick survey of some mailings
// indicates that most Reply-To headers include a display name
// identical to the From header anyway.
var replyToAddress = header.replyTo && header.replyTo[0];
var effectiveAuthor = replyToAddress || msg.refAuthor;
switch (msg.submode) {
case 'list':
// XXX we can't do this without headers we're not retrieving,
// fall through for now.
case null:
case 'sender':
rTo = [effectiveAuthor];
rCc = rBcc = [];
break;
case 'all':
// No need to change the lists if the author is already on the
// reply lists.
//
// nb: Our logic here is fairly simple; Thunderbird's
// nsMsgCompose.cpp does a lot of checking that we should
// audit, although much of it could just be related to its
// much more extensive identity support.
if (checkIfAddressListContainsAddress(header.to,
effectiveAuthor) ||
checkIfAddressListContainsAddress(header.cc,
effectiveAuthor)) {
rTo = header.to;
}
// add the author as the first 'to' person
else {
if (header.to && header.to.length)
rTo = [effectiveAuthor].concat(header.to);
else
rTo = [effectiveAuthor];
}
// For reply-all, don't reply to your own address.
var notYourIdentity = function(person) {
return person.address !== identity.address;
};
rTo = rTo.filter(notYourIdentity);
rCc = (header.cc || []).filter(notYourIdentity);
rBcc = header.bcc;
break;
}
var referencesStr;
if (bodyInfo.references) {
referencesStr = bodyInfo.references.concat([msg.refGuid])
.map(function(x) { return '<' + x + '>'; })
.join(' ');
}
else if (msg.refGuid) {
referencesStr = '<' + msg.refGuid + '>';
}
// ActiveSync does not thread so good
else {
referencesStr = '';
}
req.active = null;
self.__sendMessage({
type: 'composeBegun',
handle: msg.handle,
error: null,
identity: identity,
subject: $mailchew.generateReplySubject(msg.refSubject),
// blank lines at the top are baked in
body: $mailchew.generateReplyBody(
bodyInfo.bodyReps, effectiveAuthor, msg.refDate,
identity, msg.refGuid),
to: rTo,
cc: rCc,
bcc: rBcc,
referencesStr: referencesStr,
attachments: [],
});
}
else {
req.active = null;
self.__sendMessage({
type: 'composeBegun',
handle: msg.handle,
error: null,
identity: identity,
subject: $mailchew.generateForwardSubject(msg.refSubject),
// blank lines at the top are baked in by the func
body: $mailchew.generateForwardMessage(
msg.refAuthor, msg.refDate, msg.refSubject,
header, bodyInfo, identity),
// forwards have no assumed envelope information
to: [],
cc: [],
bcc: [],
// XXX imitate Thunderbird current or previous behaviour; I
// think we ended up linking forwards into the conversation
// they came from, but with an extra header so that it was
// possible to detect it was a forward.
references: null,
attachments: [],
});
}
});
}.bind(this));
},
_cmd_attachBlobToDraft: function(msg) {
// for ordering consistency reasons with other draft logic, this needs to
// require composer as a dependency too.
require(['./drafts/composer'], function ($composer) {
var draftReq = this._pendingRequests[msg.draftHandle];
if (!draftReq)
return;
this.universe.attachBlobToDraft(
draftReq.account,
draftReq.persistedNamer,
msg.attachmentDef,
function (err) {
this.__sendMessage({
type: 'attachedBlobToDraft',
// Note! Our use of 'msg' here means that our reference to the Blob
// will be kept alive slightly longer than the job keeps it alive,
// but just slightly.
handle: msg.handle,
draftHandle: msg.draftHandle,
err: err
});
}.bind(this));
}.bind(this));
},
_cmd_detachAttachmentFromDraft: function(msg) {
// for ordering consistency reasons with other draft logic, this needs to
// require composer as a dependency too.
require(['./drafts/composer'], function ($composer) {
var req = this._pendingRequests[msg.draftHandle];
if (!req)
return;
this.universe.detachAttachmentFromDraft(
req.account,
req.persistedNamer,
msg.attachmentIndex,
function (err) {
this.__sendMessage({
type: 'detachedAttachmentFromDraft',
handle: msg.handle,
draftHandle: msg.draftHandle,
err: err
});
}.bind(this));
}.bind(this));
},
_cmd_resumeCompose: function mb__cmd_resumeCompose(msg) {
var req = this._pendingRequests[msg.handle] = {
type: 'compose',
active: 'resume',
account: null,
persistedNamer: msg.messageNamer,
die: false
};
// NB: We are not acquiring the folder mutex here because
var account = req.account =
this.universe.getAccountForMessageSuid(msg.messageNamer.suid);
var folderStorage = this.universe.getFolderStorageForMessageSuid(
msg.messageNamer.suid);
var self = this;
folderStorage.runMutexed('resumeCompose', function(callWhenDone) {
function fail() {
self.__sendMessage({
type: 'composeBegun',
handle: msg.handle,
error: 'no-message'
});
callWhenDone();
}
folderStorage.getMessage(msg.messageNamer.suid, msg.messageNamer.date,
function(res) {
try {
if (!res.header || !res.body) {
fail();
return;
}
var header = res.header, body = res.body;
// -- convert from header/body rep to compose rep
var composeBody = {
text: '',
html: null,
};
// Body structure should be guaranteed, but add some checks.
if (body.bodyReps.length >= 1 &&
body.bodyReps[0].type === 'plain' &&
body.bodyReps[0].content.length === 2 &&
body.bodyReps[0].content[0] === 0x1) {
composeBody.text = body.bodyReps[0].content[1];
}
// HTML is optional, but if present, should satisfy our guard
if (body.bodyReps.length == 2 &&
body.bodyReps[1].type === 'html') {
composeBody.html = body.bodyReps[1].content;
}
var attachments = [];
body.attachments.forEach(function(att) {
attachments.push({
name: att.name,
blob: {
size: att.sizeEstimate,
type: att.type
}
});
});
req.active = null;
self.__sendMessage({
type: 'composeBegun',
handle: msg.handle,
error: null,
identity: account.identities[0],
subject: header.subject,
body: composeBody,
to: header.to,
cc: header.cc,
bcc: header.bcc,
referencesStr: body.references,
attachments: attachments,
sendStatus: header.sendStatus
});
callWhenDone();
}
catch (ex) {
fail(); // calls callWhenDone
}
});
});
},
/**
* Save a draft, delete a draft, or try and send a message.
*
* Drafts are saved in our IndexedDB storage. This is notable because we are
* told about attachments via their Blobs.
*/
_cmd_doneCompose: function mb__cmd_doneCompose(msg) {
require(['./drafts/composer'], function ($composer) {
var req = this._pendingRequests[msg.handle], self = this;
if (!req) {
return;
}
if (msg.command === 'die') {
if (req.active) {
req.die = true;
}
else {
delete this._pendingRequests[msg.handle];
}
return;
}
var account;
if (msg.command === 'delete') {
function sendDeleted() {
self.__sendMessage({
type: 'doneCompose',
handle: msg.handle
});
}
if (req.persistedNamer) {
account = this.universe.getAccountForMessageSuid(
req.persistedNamer.suid);
this.universe.deleteDraft(account, req.persistedNamer, sendDeleted);
}
else {
sendDeleted();
}
delete this._pendingRequests[msg.handle];
// XXX if we have persistedFolder/persistedUID, enqueue a delete of that
// message and try and execute it.
return;
}
var wireRep = msg.state;
account = this.universe.getAccountForSenderIdentityId(wireRep.senderId);
var identity = this.universe.getIdentityForSenderIdentityId(
wireRep.senderId);
if (msg.command === 'send') {
// To enqueue a message for sending:
// 1. Save the draft.
// 2. Move the draft to the outbox.
// 3. Fire off a job to send pending outbox messages.
req.persistedNamer = this.universe.saveDraft(
account, req.persistedNamer, wireRep,
function(err, newRecords) {
req.active = null;
if (req.die) {
delete this._pendingRequests[msg.handle];
}
var outboxFolder = account.getFirstFolderWithType('outbox');
this.universe.moveMessages([req.persistedNamer], outboxFolder.id);
// We only want to display notifications if the universe
// is online, i.e. we expect this sendOutboxMessages
// invocation to actually fire immediately. If we're in
// airplane mode, for instance, this job won't actually
// run until we're online, in which case it no longer
// makes sense to emit notifications for this job.
this.universe.sendOutboxMessages(account, {
reason: 'moved to outbox',
emitNotifications: this.universe.online
});
}.bind(this));
var initialSendStatus = {
accountId: account.id,
suid: req.persistedNamer.suid,
state: (this.universe.online ? 'sending' : 'pending'),
emitNotifications: true
};
// Send 'doneCompose' nearly immediately, as saveDraft might
// take a while to complete if other stuff is going on. We'll
// pass along the initialSendStatus so that we can immediately
// display status information.
this.__sendMessage({
type: 'doneCompose',
handle: msg.handle,
sendStatus: initialSendStatus
});
// Broadcast the send status immediately here as well.
this.universe.__notifyBackgroundSendStatus(initialSendStatus);
}
else if (msg.command === 'save') {
// Save the draft, updating our persisted namer.
req.persistedNamer = this.universe.saveDraft(
account, req.persistedNamer, wireRep,
function(err) {
req.active = null;
if (req.die)
delete self._pendingRequests[msg.handle];
self.__sendMessage({
type: 'doneCompose',
handle: msg.handle
});
});
}
}.bind(this));
},
notifyCronSyncStart: function mb_notifyCronSyncStart(accountIds) {
this.__sendMessage({
type: 'cronSyncStart',
accountIds: accountIds
});
},
notifyCronSyncStop: function mb_notifyCronSyncStop(accountsResults) {
this.__sendMessage({
type: 'cronSyncStop',
accountsResults: accountsResults
});
},
/**
* Notify the frontend about the status of message sends. Data has
* keys like 'state', 'error', etc, per the sendOutboxMessages job.
*/
notifyBackgroundSendStatus: function(data) {
this.__sendMessage({
type: 'backgroundSendStatus',
data: data
});
}
};
}); // end define