/** * **/ 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