Permalink
Cannot retrieve contributors at this time
1720 lines (1595 sloc)
58.1 KB
| /** | |
| * @module | |
| */ | |
| define(function(require, exports) { | |
| 'use strict'; | |
| var logic = require('logic'); | |
| // XXX proper logging configuration for the front-end too once things start | |
| // working happily. | |
| logic.realtimeLogEverything = true; | |
| // Use a relative link so that consumers do not need to create | |
| // special config to use main-frame-setup. | |
| const addressparser = require('./ext/addressparser'); | |
| const evt = require('evt'); | |
| const MailFolder = require('./clientapi/mail_folder'); | |
| const MailConversation = require('./clientapi/mail_conversation'); | |
| const MailMessage = require('./clientapi/mail_message'); | |
| const ContactCache = require('./clientapi/contact_cache'); | |
| const UndoableOperation = require('./clientapi/undoable_operation'); | |
| const AccountsViewSlice = require('./clientapi/accounts_view_slice'); | |
| const FoldersListView = require('./clientapi/folders_list_view'); | |
| const ConversationsListView = require('./clientapi/conversations_list_view'); | |
| const MessagesListView = require('./clientapi/messages_list_view'); | |
| const RawListView = require('./clientapi/raw_list_view'); | |
| const MessageComposition = require('./clientapi/message_composition'); | |
| const { accountIdFromFolderId, accountIdFromConvId, accountIdFromMessageId, | |
| convIdFromMessageId } = | |
| require('./id_conversions'); | |
| const Linkify = require('./clientapi/bodies/linkify'); | |
| /** | |
| * Given a list of MailFolders (that may just be null and not a list), map those | |
| * to the folder id's. | |
| */ | |
| let normalizeFoldersToIds = (folders) => { | |
| if (!folders) { | |
| return folders; | |
| } | |
| return folders.map(folder => folder.id); | |
| }; | |
| // For testing | |
| exports._MailFolder = MailFolder; | |
| const LEGAL_CONFIG_KEYS = ['debugLogging']; | |
| /** | |
| * The public API exposed to the client via the MailAPI global. | |
| * | |
| * TODO: Implement a failsafe timeout mechanism for returning Promises for | |
| * requests that will timeout and reject or something. The idea is to allow | |
| * code that | |
| * | |
| * @constructor | |
| * @memberof module:mailapi | |
| */ | |
| function MailAPI() { | |
| evt.Emitter.call(this); | |
| logic.defineScope(this, 'MailAPI', {}); | |
| this._nextHandle = 1; | |
| /** | |
| * @type {Map<BridgeHandle, Object>} | |
| * | |
| * Holds live list views (what were formerly called slices) and live tracked | |
| * one-off items (ex: viewConversation/friends that call | |
| * _getItemAndTrackUpdates). | |
| * | |
| * In many ways this is nearly identically to _pendingRequests, but this was | |
| * split off because different semantics were originally intended. Probably | |
| * it makes sense to keep this for "persistent subscriptions" and eventually | |
| * replace things using _pendingRequests with an explicitly supported | |
| * co.wrap() idiom. | |
| */ | |
| this._trackedItemHandles = new Map(); | |
| this._pendingRequests = {}; | |
| this._liveBodies = {}; | |
| // Store bridgeSend messages received before back end spawns. | |
| this._storedSends = []; | |
| this._processingMessage = null; | |
| /** | |
| * List of received messages whose processing is being deferred because we | |
| * still have a message that is actively being processed, as stored in | |
| * `_processingMessage`. | |
| */ | |
| this._deferredMessages = []; | |
| /** | |
| * @dict[ | |
| * @key[debugLogging] | |
| * @key[checkInterval] | |
| * ]{ | |
| * Configuration data. This is currently populated by data from | |
| * `MailUniverse.exposeConfigForClient` by the code that constructs us. In | |
| * the future, we will probably want to ask for this from the `MailUniverse` | |
| * directly over the wire. | |
| * | |
| * This should be treated as read-only. | |
| * } | |
| */ | |
| this.config = {}; | |
| /** | |
| * Has the MailUniverse come up and reported in to us and provided us with the | |
| * initial config? You can use latestOnce('configLoaded') for this purpose. | |
| * Note that you don't need to wait for this if you have other API calls to | |
| * make since all messages are buffered and released once the universe is | |
| * available. | |
| */ | |
| this.configLoaded = false; | |
| /** | |
| * Has the MailUniverse finished loading its list of accounts and their | |
| * folders and told us about them and our `accounts` view and each of their | |
| * `folders` views has completed populating? You can use | |
| * latestOnce('accountsLoaded') in order to be notified once it has occurred | |
| * (or immediately if it has already occurred). | |
| */ | |
| this.accountsLoaded = false; | |
| /* PROPERLY DOCUMENT EVENT 'badlogin' | |
| * @func[ | |
| * @args[ | |
| * @param[account MailAccount] | |
| * ] | |
| * ]{ | |
| * A callback invoked when we fail to login to an account and the server | |
| * explicitly told us the login failed and we have no reason to suspect | |
| * the login was temporarily disabled. | |
| * | |
| * The account is put in a disabled/offline state until such time as the | |
| * | |
| * } | |
| */ | |
| ContactCache.init(); | |
| // Default slices: | |
| this.accounts = this.viewAccounts({ autoViewFolders: true }); | |
| } | |
| exports.MailAPI = MailAPI; | |
| MailAPI.prototype = evt.mix(/** @lends module:mailapi.MailAPI.prototype */ { | |
| toString: function() { | |
| return '[MailAPI]'; | |
| }, | |
| toJSON: function() { | |
| return { type: 'MailAPI' }; | |
| }, | |
| /** | |
| * Invoked by main-frame-setup when it's done poking things into us so that | |
| * we can set flags and emit events and such. We can probably also move more | |
| * of its logic into this file if it makes sense. | |
| */ | |
| __universeAvailable: function() { | |
| this.configLoaded = true; | |
| this.emit('configLoaded'); | |
| logic(this, 'configLoaded'); | |
| // wait for the account view to be fully populated | |
| this.accounts.latestOnce('complete', () => { | |
| // wait for all of the accounts to have their folder views fully populated | |
| Promise.all(this.accounts.items.map((account) => { | |
| return new Promise((resolve) => { | |
| account.folders.latestOnce('complete', resolve); | |
| }); | |
| })).then(() => { | |
| this.accountsLoaded = true; | |
| logic(this, 'accountsLoaded'); | |
| this.emit('accountsLoaded'); | |
| }); | |
| }); | |
| }, | |
| // This exposure as "utils" exists for legacy reasons right now, we should | |
| // probably just move consumers to directly require the module. | |
| utils: Linkify, | |
| /** | |
| * Return a Promise that will be resolved with a guaranteed-alive MailAccount | |
| * instance from our `accounts` view. You would use this if you can't | |
| * guarantee that `accountsLoaded` is already true or if the account is | |
| * potentially in the process of being created. | |
| */ | |
| eventuallyGetAccountById: function(accountId) { | |
| return this.accounts.eventuallyGetAccountById(accountId); | |
| }, | |
| /** | |
| * Return a Promise that will be resolved with a guaranteed-alive MailFolder | |
| * instance from one the corresponding `folders` view on the `MailAccount` | |
| * from our `accounts` view that owns the folder. You would use this if you | |
| * can't guarantee that `accountsLoaded` is already true. Some implementation | |
| * changes are required if you want this to also cover folders that are not | |
| * yet synchronized. | |
| */ | |
| eventuallyGetFolderById: function(folderId) { | |
| var accountId = accountIdFromFolderId(folderId); | |
| return this.accounts.eventuallyGetAccountById(accountId).then( | |
| function gotAccount(account) { | |
| return account.folders.eventuallyGetFolderById(folderId); | |
| } | |
| ); | |
| }, | |
| /** | |
| * Synchronous version of eventuallyGetFolderById that will return null if | |
| * either the account or folder don't currently exist. If your logic is gated | |
| * by latestOnce('accountsLoaded') and this isn't a newly created account, | |
| * then you should be safe in using this. Otherwise wait for `accountsLoaded` | |
| * or use `eventuallyGetFolderById`. | |
| */ | |
| getFolderById: function(folderId) { | |
| const accountId = accountIdFromFolderId(folderId); | |
| const account = this.accounts.getAccountById(accountId); | |
| return account && account.folders.getFolderById(folderId); | |
| }, | |
| /** | |
| * Convert the folder id's for a message into MailFolder instances by looking | |
| * them up from the account's folders list view. | |
| * | |
| * XXX deal with the potential asynchrony of this method being called before | |
| * the account is known to us. We should generally be fine, but we don't have | |
| * the guards in place to actually protect us. | |
| */ | |
| _mapLabels: function(messageId, folderIds) { | |
| let accountId = accountIdFromMessageId(messageId); | |
| let account = this.accounts.getAccountById(accountId); | |
| if (!account) { | |
| console.warn('the possible has happened; unable to find account with id', | |
| accountId); | |
| } | |
| let folders = account.folders; | |
| return Array.from(folderIds).map((folderId) => { | |
| return folders.getFolderById(folderId); | |
| }); | |
| }, | |
| /** | |
| * Send a message over/to the bridge. The idea is that we (can) communicate | |
| * with the backend using only a postMessage-style JSON channel. | |
| */ | |
| __bridgeSend: function(msg) { | |
| // This method gets clobbered eventually once back end worker is ready. | |
| // Until then, it will store calls to send to the back end. | |
| this._storedSends.push(msg); | |
| }, | |
| /** | |
| * Process a message received from the bridge. | |
| */ | |
| __bridgeReceive: function(msg) { | |
| // Pong messages are used for tests | |
| if (this._processingMessage && msg.type !== 'pong') { | |
| logic(this, 'deferMessage', { type: msg.type }); | |
| this._deferredMessages.push(msg); | |
| } | |
| else { | |
| logic(this, 'immediateProcess', { type: msg.type }); | |
| this._processMessage(msg); | |
| } | |
| }, | |
| _processMessage: function(msg) { | |
| var methodName = '_recv_' + msg.type; | |
| if (!(methodName in this)) { | |
| logic.fail(new Error('Unsupported message type:', msg.type)); | |
| return; | |
| } | |
| try { | |
| logic(this, 'processMessage', { type: msg.type }); | |
| var promise = this[methodName](msg); | |
| if (promise && promise.then) { | |
| this._processingMessage = promise; | |
| promise.then(this._doneProcessingMessage.bind(this, msg)); | |
| } | |
| } | |
| catch (ex) { | |
| logic( | |
| this, 'processMessageError', | |
| { | |
| type: msg.type, | |
| ex, | |
| stack: ex.stack | |
| }); | |
| return; | |
| } | |
| }, | |
| _doneProcessingMessage: function(msg) { | |
| if (this._processingMessage && this._processingMessage !== msg) { | |
| throw new Error('Mismatched message completion!'); | |
| } | |
| this._processingMessage = null; | |
| while (this._processingMessage === null && this._deferredMessages.length) { | |
| this._processMessage(this._deferredMessages.shift()); | |
| } | |
| }, | |
| /** @see ContactCache.shoddyAutocomplete */ | |
| shoddyAutocomplete: function(phrase) { | |
| return ContactCache.shoddyAutocomplete(phrase); | |
| }, | |
| /** | |
| * Return a promise that's resolved with a MailConversation instance that is | |
| * live-updating with events until `release` is called on it. | |
| */ | |
| getConversation: function(conversationId, priorityTags) { | |
| // We need the account for the conversation in question to be loaded for | |
| // safety, dependency reasons. | |
| return this.eventuallyGetAccountById(accountIdFromConvId(conversationId)) | |
| .then(() => { | |
| // account is ignored, we just needed to ensure it existed for | |
| // _mapLabels to be a friendly, happy, synchronous API. | |
| return this._getItemAndTrackUpdates( | |
| 'conv', conversationId, MailConversation, priorityTags); | |
| }); | |
| }, | |
| /** | |
| * Return a promise that's resolved with a MailMessage instance that is | |
| * live-updating with events until `release` is called on it. | |
| * | |
| * @param {[MessageId, DateMS]} messageNamer | |
| */ | |
| getMessage: function(messageNamer, priorityTags) { | |
| let messageId = messageNamer[0]; | |
| // We need the account for the conversation in question to be loaded for | |
| // safety, dependency reasons. | |
| return this.eventuallyGetAccountById(accountIdFromMessageId(messageId)) | |
| .then(() => { | |
| // account is ignored, we just needed to ensure it existed for | |
| // _mapLabels to be a friendly, happy, synchronous API. | |
| return this._getItemAndTrackUpdates( | |
| 'msg', messageNamer, MailMessage, priorityTags); | |
| }); | |
| }, | |
| /** | |
| * Sends a message with a freshly allocated single-use handle, returning a | |
| * Promise that will be resolved when the MailBridge responds to the message. | |
| * (Someday it may also be rejected if we lose the back-end.) | |
| */ | |
| _sendPromisedRequest: function(sendMsg) { | |
| return new Promise((resolve) => { | |
| let handle = sendMsg.handle = this._nextHandle++; | |
| this._pendingRequests[handle] = { | |
| type: sendMsg.type, | |
| resolve | |
| }; | |
| this.__bridgeSend(sendMsg); | |
| }); | |
| }, | |
| _recv_promisedResult: function(msg) { | |
| let handle = msg.handle; | |
| let pending = this._pendingRequests[handle]; | |
| delete this._pendingRequests[handle]; | |
| pending.resolve(msg.data); | |
| }, | |
| /** | |
| * Create an UndoableOperation for synchronous return to the caller that will | |
| * have its actual tasks to undo filled in asynchronously. Idiom glue logic. | |
| */ | |
| _sendUndoableRequest: function(undoableInfo, requestPayload) { | |
| let id = this._nextHandle; | |
| let undoableTasksPromise = this._sendPromisedRequest(requestPayload); | |
| let undoableOp = new UndoableOperation({ | |
| api: this, | |
| id, | |
| operation: undoableInfo.operation, | |
| affectedType: undoableInfo.affectedType, | |
| affectedCount: undoableInfo.affectedCount, | |
| undoableTasksPromise | |
| }); | |
| this.emit('undoableOp', undoableOp); | |
| return undoableOp; | |
| }, | |
| __scheduleUndoTasks: function(undoableOp, undoTasks) { | |
| this.emit('undoing', undoableOp); | |
| this.__bridgeSend({ | |
| type: 'undo', | |
| undoTasks | |
| }); | |
| }, | |
| /** | |
| * Normalize conversation/message references to our list of | |
| * conversation-with-selector objects of the form/type { id, messageIds, | |
| * messageSelector }. | |
| */ | |
| _normalizeConversationSelectorArgs: function(arrayOfStuff, args) { | |
| let { detectType: argDetect, conversations: argConversations, | |
| messages: argMessages, messageSelector } = args; | |
| let convSelectors; | |
| if (arrayOfStuff) { | |
| argDetect = arrayOfStuff; | |
| } | |
| if (argDetect) { | |
| if (argDetect[0] instanceof MailMessage) { | |
| argMessages = argDetect; | |
| } else if (argDetect[0] instanceof MailConversation) { | |
| argConversations = argDetect; | |
| } | |
| } | |
| let affectedType; | |
| let affectedCount = 0; | |
| if (argConversations) { | |
| affectedType = 'conversation'; | |
| affectedCount = argConversations.length; | |
| convSelectors = argConversations.map((x) => { | |
| return { | |
| id: x.id, | |
| messageSelector | |
| }; | |
| }); | |
| } else if (argMessages) { | |
| affectedType = 'message'; | |
| affectedCount = argMessages.length; | |
| convSelectors = []; | |
| let selectorByConvId = new Map(); | |
| for (let message of argMessages) { | |
| let convId = convIdFromMessageId(message.id); | |
| let selector = selectorByConvId.get(convId); | |
| if (!selector) { | |
| selector = { | |
| id: convId, | |
| messageIds: [message.id] | |
| }; | |
| selectorByConvId.set(convId, selector); | |
| convSelectors.push(selector); | |
| } else { | |
| selector.messageIds.push(message.id); | |
| } | |
| } | |
| } else { | |
| throw new Error('Weird conversation/message selector.'); | |
| } | |
| return { convSelectors, affectedType, affectedCount }; | |
| }, | |
| _recv_broadcast: function(msg) { | |
| let { name, data } = msg.payload; | |
| this.emit(name, data); | |
| }, | |
| /** | |
| * Ask the back-end for an item by its id. The current state will be loaded | |
| * from the db and then logically consistent updates provided until release | |
| * is called on the object. | |
| * | |
| * In the future we may support also taking an existing wireRep so that the | |
| * object can be provided synchronously. I want to try to avoid that at first | |
| * because it's the type of thing that really wants to be implemented when | |
| * we've got our unit tests stood up again. | |
| * | |
| * `_cleanupContext` should be invoked by the release method of whatever | |
| * object we create when all done. | |
| * | |
| * XXX there's a serious potential for resource-leak/clobbering races where by | |
| * the time we resolve our promise the caller will not correctly call release | |
| * on our value or we'll end up clobbering the value from a chronologically | |
| * later call to our method. | |
| */ | |
| _getItemAndTrackUpdates: function(itemType, itemId, itemConstructor, | |
| priorityTags) { | |
| return new Promise((resolve, reject) => { | |
| let handle = this._nextHandle++; | |
| this._trackedItemHandles.set(handle, { | |
| type: itemType, | |
| id: itemId, | |
| callback: (msg) => { | |
| if (msg.error || !msg.data) { | |
| reject( | |
| new Error('track problem, error: ' + msg.error + ' has data?: ' + | |
| !!msg.data)); | |
| return; | |
| } | |
| let obj = new itemConstructor( | |
| this, msg.data.state, msg.data.overlays, null, handle); | |
| resolve(obj); | |
| this._trackedItemHandles.set(handle, { | |
| type: itemType, | |
| id: itemId, | |
| obj: obj | |
| }); | |
| } | |
| }); | |
| this.__bridgeSend({ | |
| type: 'getItemAndTrackUpdates', | |
| handle: handle, | |
| itemType: itemType, | |
| itemId: itemId, | |
| priorityTags | |
| }); | |
| return handle; | |
| }); | |
| }, | |
| _recv_gotItemNowTrackingUpdates: function(msg) { | |
| let details = this._trackedItemHandles.get(msg.handle); | |
| details.callback(msg); | |
| }, | |
| /** | |
| * Internal-only API to update the priority associated with an instantiated | |
| * object. | |
| */ | |
| _updateTrackedItemPriorityTags: function(handle, priorityTags) { | |
| this.__bridgeSend({ | |
| type: 'updateTrackedItemPriorityTags', | |
| handle: handle, | |
| priorityTags: priorityTags | |
| }); | |
| }, | |
| // update event for list views. This used to be shared logic with updateItem | |
| // but when overlays came into the picture the divergence got too crazy. | |
| _recv_update: function(msg) { | |
| let details = this._trackedItemHandles.get(msg.handle); | |
| if (details && details.obj) { | |
| let obj = details.obj; | |
| let data = msg.data; | |
| obj.__update(data); | |
| } | |
| }, | |
| // update event for tracked items (rather than list views) | |
| _recv_updateItem: function(msg) { | |
| let details = this._trackedItemHandles.get(msg.handle); | |
| if (details && details.obj) { | |
| let obj = details.obj; | |
| let data = msg.data; | |
| if (data === null) { | |
| // - null means removal | |
| // TODO: consider whether our semantics should be self-releasing in this | |
| // case. For now we will leave it up to the caller. | |
| obj.emit('remove', obj); | |
| } else { | |
| // - non-null means it's an update! | |
| if (data.state) { | |
| obj.__update(data.state); | |
| } | |
| if (data.overlays) { | |
| obj.__updateOverlays(data.overlays); | |
| } | |
| obj.serial++; | |
| obj.emit('change', obj); | |
| } | |
| } | |
| }, | |
| _cleanupContext: function(handle) { | |
| this.__bridgeSend({ | |
| type: 'cleanupContext', | |
| handle: handle | |
| }); | |
| }, | |
| /** | |
| * The mailbridge response to a "cleanupContext" command, triggered by a call | |
| * to our sibling `_cleanupContext` function which should be invoked by public | |
| * `release` calls. | |
| * | |
| * TODO: Conclusively decide whether it could make sense for this, or a | |
| * variant of this for cases where the mailbridge/backend can send effectively | |
| * unsolicited notifications of this. | |
| */ | |
| _recv_contextCleanedUp: function(msg) { | |
| this._trackedItemHandles.delete(msg.handle); | |
| }, | |
| _downloadBodyReps: function(messageId, messageDate) { | |
| this.__bridgeSend({ | |
| type: 'downloadBodyReps', | |
| id: messageId, | |
| date: messageDate | |
| }); | |
| }, | |
| _downloadAttachments: function(downloadReq) { | |
| return this._sendPromisedRequest({ | |
| type: 'downloadAttachments', | |
| downloadReq | |
| }); | |
| }, | |
| /** | |
| * Given a user's email address, try and see if we can autoconfigure the | |
| * account and what information we'll need to configure it, specifically | |
| * a password or if XOAuth2 credentials will be needed. | |
| * | |
| * @param {Object} details | |
| * @param {String} details.emailAddress | |
| * The user's email address. | |
| * @return {Promise<Object>} | |
| * A promise that will be resolved with an object like so: | |
| * | |
| * No autoconfig information is available and the user has to do manual | |
| * setup: | |
| * | |
| * { | |
| * result: 'no-config-info', | |
| * configInfo: null | |
| * } | |
| * | |
| * Autoconfig information is available and to complete the autoconfig | |
| * we need the user's password. For IMAP and POP3 this means we know | |
| * everything we need and can actually create the account. For ActiveSync | |
| * we actually need the password to try and perform autodiscovery. | |
| * | |
| * { | |
| * result: 'need-password', | |
| * configInfo: { incoming, outgoing } | |
| * } | |
| * | |
| * Autoconfig information is available and XOAuth2 authentication should | |
| * be attempted and those credentials then provided to us. | |
| * | |
| * { | |
| * result: 'need-oauth2', | |
| * configInfo: { | |
| * incoming, | |
| * outgoing, | |
| * oauth2Settings: { | |
| * secretGroup: 'google' or 'microsoft' or other arbitrary string, | |
| * authEndpoint: 'url to the auth endpoint', | |
| * tokenEndpoint: 'url to where you ask for tokens', | |
| * scope: 'space delimited list of scopes to request' | |
| * } | |
| * } | |
| * } | |
| * | |
| * A `source` property will also be present in the result object. Its | |
| * value will be one of: 'hardcoded', 'local', 'ispdb', | |
| * 'autoconfig-subdomain', 'autoconfig-wellknown', 'mx local', 'mx ispdb', | |
| * 'autodiscover'. | |
| */ | |
| learnAboutAccount: function(details) { | |
| return this._sendPromisedRequest({ | |
| type: 'learnAboutAccount', | |
| details | |
| }); | |
| }, | |
| /** | |
| * Try to create an account. There is currently no way to abort the process | |
| * of creating an account. You really want to use learnAboutAccount before | |
| * you call this unless you are an automated test. | |
| * | |
| * @typedef[AccountCreationError @oneof[ | |
| * @case['offline']{ | |
| * We are offline and have no network access to try and create the | |
| * account. | |
| * } | |
| * @case['no-dns-entry']{ | |
| * We couldn't find the domain name in question, full stop. | |
| * | |
| * Not currently generated; eventually desired because it suggests a typo | |
| * and so a specialized error message is useful. | |
| * } | |
| * @case['no-config-info']{ | |
| * We were unable to locate configuration information for the domain. | |
| * } | |
| * @case['unresponsive-server']{ | |
| * Requests to the server timed out. AKA we sent packets into a black | |
| * hole. | |
| * } | |
| * @case['port-not-listening']{ | |
| * Attempts to connect to the given port on the server failed. We got | |
| * packets back rejecting our connection. | |
| * | |
| * Not currently generated; primarily desired because it is very useful if | |
| * we are domain guessing. Also desirable for error messages because it | |
| * suggests a user typo or the less likely server outage. | |
| * } | |
| * @case['bad-security']{ | |
| * We were able to connect to the port and initiate TLS, but we didn't | |
| * like what we found. This could be a mismatch on the server domain, | |
| * a self-signed or otherwise invalid certificate, insufficient crypto, | |
| * or a vulnerable server implementation. | |
| * } | |
| * @case['bad-user-or-pass']{ | |
| * The username and password didn't check out. We don't know which one | |
| * is wrong, just that one of them is wrong. | |
| * } | |
| * @case['bad-address']{ | |
| * The e-mail address provided was rejected by the SMTP probe. | |
| * } | |
| * @case['pop-server-not-great']{ | |
| * The POP3 server doesn't support IDLE and TOP, so we can't use it. | |
| * } | |
| * @case['imap-disabled']{ | |
| * IMAP support is not enabled for the Gmail account in use. | |
| * } | |
| * @case['pop3-disabled']{ | |
| * POP3 support is not enabled for the Gmail account in use. | |
| * } | |
| * @case['needs-oauth-reauth']{ | |
| * The OAUTH refresh token was invalid, or there was some problem with | |
| * the OAUTH credentials provided. The user needs to go through the | |
| * OAUTH flow again. | |
| * } | |
| * @case['not-authorized']{ | |
| * The username and password are correct, but the user isn't allowed to | |
| * access the mail server. | |
| * } | |
| * @case['server-problem']{ | |
| * We were able to talk to the "server" named in the details object, but | |
| * we encountered some type of problem. The details object will also | |
| * include a "status" value. | |
| * } | |
| * @case['server-maintenance']{ | |
| * The server appears to be undergoing maintenance, at least for this | |
| * account. We infer this if the server is telling us that login is | |
| * disabled in general or when we try and login the message provides | |
| * positive indications of some type of maintenance rather than a | |
| * generic error string. | |
| * } | |
| * @case['user-account-exists']{ | |
| * If the user tries to create an account which is already configured. | |
| * Should not be created. We will show that account is already configured | |
| * } | |
| * @case['unknown']{ | |
| * We don't know what happened; count this as our bug for not knowing. | |
| * } | |
| * @case[null]{ | |
| * No error, the account was created and everything is terrific. | |
| * } | |
| * ]] | |
| * | |
| * @param {Object} details | |
| * @param {String} details.emailAddress | |
| * @param {String} [details.password] | |
| * The user's password | |
| * @param {Object} [configInfo] | |
| * If continuing an autoconfig initiated by learnAboutAccount, the | |
| * configInfo it returned as part of its results, although you will need | |
| * to poke the following structured properties in if you're doing the oauth2 | |
| * thing: | |
| * | |
| * { | |
| * oauth2Secrets: { clientId, clientSecret } | |
| * oauth2Tokens: { accessToken, refreshToken, expireTimeMS } | |
| * } | |
| * | |
| * If performing a manual config, a manually created configInfo object of | |
| * the following form: | |
| * | |
| * { | |
| * incoming: { hostname, port, socketType, username, password } | |
| * outgoing: { hostname, port, socketType, username, password } | |
| * } | |
| * | |
| * | |
| * | |
| * @param {Function} callback | |
| * The callback to invoke upon success or failure. The callback will be | |
| * called with 2 arguments in the case of failure: the error string code, | |
| * and the error details object. | |
| * | |
| * | |
| * @args[ | |
| * @param[details @dict[ | |
| * @key[displayName String]{ | |
| * The name the (human, per EULA) user wants to be known to the world | |
| * as. | |
| * } | |
| * @key[emailAddress String] | |
| * @key[password String] | |
| * ]] | |
| * @param[callback @func[ | |
| * @args[ | |
| * @param[error AccountCreationError] | |
| * @param[errorDetails @dict[ | |
| * @key[server #:optional String]{ | |
| * The server we had trouble talking to. | |
| * } | |
| * @key[status #:optional @oneof[Number String]]{ | |
| * The HTTP status code number, or "timeout", or something otherwise | |
| * providing detailed additional information about the error. This | |
| * is usually too technical to be presented to the user, but is | |
| * worth encoding with the error name proper if possible. | |
| * } | |
| * ]] | |
| * ] | |
| * ] | |
| * ] | |
| */ | |
| tryToCreateAccount: function(userDetails, domainInfo) { | |
| return this._sendPromisedRequest({ | |
| type: 'tryToCreateAccount', | |
| userDetails, | |
| domainInfo | |
| }).then((result) => { | |
| if (result.accountId) { | |
| return this.accounts.eventuallyGetAccountById(result.accountId).then( | |
| (account) => { | |
| return { | |
| error: null, | |
| errorDetails: null, | |
| account | |
| }; | |
| } | |
| ); | |
| } else { | |
| return { | |
| error: result.error, | |
| errorDetails: result.errorDetails | |
| }; | |
| } | |
| }); | |
| }, | |
| _clearAccountProblems: function ma__clearAccountProblems(account, callback) { | |
| var handle = this._nextHandle++; | |
| this._pendingRequests[handle] = { | |
| type: 'clearAccountProblems', | |
| callback: callback, | |
| }; | |
| this.__bridgeSend({ | |
| type: 'clearAccountProblems', | |
| accountId: account.id, | |
| handle: handle, | |
| }); | |
| }, | |
| _recv_clearAccountProblems: function ma__recv_clearAccountProblems(msg) { | |
| var req = this._pendingRequests[msg.handle]; | |
| delete this._pendingRequests[msg.handle]; | |
| req.callback && req.callback(); | |
| }, | |
| _modifyAccount: function(account, mods) { | |
| return this._sendPromisedRequest({ | |
| type: 'modifyAccount', | |
| accountId: account.id, | |
| mods | |
| }).then(() => null); | |
| }, | |
| _recreateAccount: function(account) { | |
| this.__bridgeSend({ | |
| type: 'recreateAccount', | |
| accountId: account.id, | |
| }); | |
| }, | |
| _deleteAccount: function(account) { | |
| this.__bridgeSend({ | |
| type: 'deleteAccount', | |
| accountId: account.id, | |
| }); | |
| }, | |
| _modifyIdentity: function(identity, mods) { | |
| return this._sendPromisedRequest({ | |
| type: 'modifyIdentity', | |
| identityId: identity.id, | |
| mods | |
| }).then(() => null); | |
| }, | |
| /** | |
| * Get the list of accounts. This can be used for the list of accounts in | |
| * setttings or for a folder tree where only one account's folders are visible | |
| * at a time. | |
| * | |
| * @param {Object} [opts] | |
| * @param {Boolean} [opts.autoViewFolders=false] | |
| * Should the `MailAccount` instances automatically issue viewFolders | |
| * requests and assign them to a "folders" property? | |
| */ | |
| viewAccounts: function(opts) { | |
| var handle = this._nextHandle++, | |
| view = new AccountsViewSlice(this, handle, opts); | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| this.__bridgeSend({ | |
| type: 'viewAccounts', | |
| handle | |
| }); | |
| return view; | |
| }, | |
| /** | |
| * Retrieve the entire folder hierarchy for either 'navigation' (pick what | |
| * folder to show the contents of, including unified folders), 'movetarget' | |
| * (pick target folder for moves, does not include unified folders), or | |
| * 'account' (only show the folders belonging to a given account, implies | |
| * selection). In all cases, there may exist non-selectable folders such as | |
| * the account roots or IMAP folders that cannot contain messages. | |
| * | |
| * When accounts are presented as folders via this UI, they do not expose any | |
| * of their `MailAccount` semantics. | |
| * | |
| * @args[ | |
| * @param[mode @oneof['navigation' 'movetarget' 'account'] | |
| * @param[argument #:optional]{ | |
| * Arguent appropriate to the mode; currently will only be a `MailAccount` | |
| * instance. | |
| * } | |
| * ] | |
| */ | |
| viewFolders: function(mode, accountId) { | |
| var handle = this._nextHandle++, | |
| view = new FoldersListView(this, handle); | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| this.__bridgeSend({ | |
| type: 'viewFolders', | |
| mode, | |
| handle, | |
| accountId | |
| }); | |
| return view; | |
| }, | |
| /** | |
| * View some list provided by an extension or a hack in the backend, returning | |
| * a RawListView that holds RawItem instances. If things get fancy and you | |
| * aren't dealing in "raw" things, then we might want to create Additional | |
| * explicit API calls for typing reasons. | |
| * | |
| * @param {String} namespace | |
| * Effectively identifies the extension/provider that will be providing the | |
| * data. We call it a namespace because maybe multiple extensions will | |
| * service the same namespace or something. | |
| * @param {String} name | |
| * Some string that describes to the extension(s)/provider(s) what you want | |
| * from inside their namespace. We require it to be a String so that we | |
| * can use it as a key in a Map | |
| */ | |
| viewRawList: function(namespace, name) { | |
| var handle = this._nextHandle++, | |
| view = new RawListView(this, handle); | |
| view.source = { namespace, name }; | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| this.__bridgeSend({ | |
| type: 'viewRawList', | |
| handle, | |
| namespace, | |
| name | |
| }); | |
| return view; | |
| }, | |
| /** | |
| * View the conversations in a folder. | |
| */ | |
| viewFolderConversations: function(folder) { | |
| var handle = this._nextHandle++, | |
| view = new ConversationsListView(this, handle); | |
| view.folderId = folder.id; | |
| // Hackily save off the folder as a stop-gap measure to make it easier to | |
| // describe the contents of the view until we enhance the tocMeta to | |
| // better convey this. | |
| view.folder = this.getFolderById(view.folderId); | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| this.__bridgeSend({ | |
| type: 'viewFolderConversations', | |
| folderId: folder.id, | |
| handle | |
| }); | |
| return view; | |
| }, | |
| _makeDerivedViews: function(rootView, viewSpecs) { | |
| const viewDefsWithHandles = []; | |
| const createView = (viewDef) => { | |
| const handle = this._nextHandle++; | |
| const view = new RawListView(this, handle); | |
| view.viewDef = viewDef; | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| viewDefsWithHandles.push({ | |
| handle, | |
| viewDef | |
| }); | |
| return view; | |
| }; | |
| let apiResult = { | |
| root: rootView | |
| }; | |
| for (let key of Object.keys(viewSpecs)) { | |
| let viewDefs = viewSpecs[key]; | |
| apiResult[key] = viewDefs.map(createView); | |
| } | |
| return { apiResult, viewDefsWithHandles }; | |
| }, | |
| /** | |
| * Search a folder's conversations for conversations matching the provided | |
| * filter constraints, returning a ConversationsListView. | |
| * | |
| * @param {Object} spec | |
| * @param {MailFolder} spec.folder | |
| * The folder whose messages we should search. | |
| * @param {Object) spec.filter | |
| * @param {String} [spec.filter.author] | |
| * Match against author display name or email address. | |
| * @param {String} [spec.filter.recipients] | |
| * Match against recipient display name or email addresses. | |
| * @param {String} [spec.filter.subject] | |
| * Match against the message subject. | |
| * @param {String} [spec.filter.body] | |
| * Match against the authored message body. Quoted blocks will be ignored. | |
| * @param {String} [spec.filter.bodyAndQuotes] | |
| * Match against the authored message body and any included quoted blocks. | |
| * @param {Object} spec.derivedViews | |
| * Derived view definitions. The input should look like { foo: [viewDef1, | |
| * viewDef2], bar: [viewDef3] }. When used, this will then alter the | |
| * return value of this method to be { root: theNormalView, foo: | |
| * [derivedView1, derivedView2], bar: [derivedView3] }. | |
| */ | |
| searchFolderConversations: function(spec) { | |
| var handle = this._nextHandle++, | |
| view = new ConversationsListView(this, handle); | |
| view.folderId = spec.folder.id; | |
| // Hackily save off the folder as a stop-gap measure to make it easier to | |
| // describe the contents of the view until we enhance the tocMeta to | |
| // better convey this. | |
| view.folder = this.getFolderById(view.folderId); | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| let result = view; | |
| let viewDefsWithHandles = null; | |
| if (spec.derivedViews) { | |
| ({ apiResult: result, viewDefsWithHandles } = | |
| this._makeDerivedViews(view, spec.derivedViews)); | |
| } | |
| this.__bridgeSend({ | |
| type: 'searchFolderConversations', | |
| handle, | |
| spec: { | |
| folderId: view.folderId, | |
| filter: spec.filter, | |
| }, | |
| viewDefsWithHandles | |
| }); | |
| return result; | |
| }, | |
| viewConversationMessages: function(convOrId) { | |
| var handle = this._nextHandle++, | |
| view = new MessagesListView(this, handle); | |
| view.conversationId = (typeof(convOrId) === 'string' ? convOrId : | |
| convOrId.id); | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| this.__bridgeSend({ | |
| type: 'viewConversationMessages', | |
| conversationId: view.conversationId, | |
| handle | |
| }); | |
| return view; | |
| }, | |
| /** | |
| * Search a conversations messages for messages matching the provided | |
| * filter constraints, returning a MessagesListView. | |
| * | |
| * @param {Object} spec | |
| * @param {MailFolder} spec.conversation | |
| * The conversation whose messages we should search. | |
| * @param {Object) spec.filter | |
| * @param {String} [spec.filter.author] | |
| * Match against author display name or email address. | |
| * @param {String} [spec.filter.recipients] | |
| * Match against recipient display name or email addresses. | |
| * @param {String} [spec.filter.subject] | |
| * Match against the message subject. | |
| * @param {String} [spec.filter.body] | |
| * Match against the authored message body. Quoted blocks will be ignored. | |
| * @param {String} [spec.filter.bodyAndQuotes] | |
| * Match against the authored message body and any included quoted blocks. | |
| */ | |
| searchConversationMessages: function(spec) { | |
| var handle = this._nextHandle++, | |
| view = new MessagesListView(this, handle); | |
| view.conversationId = spec.conversation.id; | |
| this._trackedItemHandles.set(handle, { obj: view }); | |
| this.__bridgeSend({ | |
| type: 'searchConversationMessages', | |
| handle, | |
| spec: { | |
| conversationId: view.conversationId, | |
| filter: spec.filter | |
| } | |
| }); | |
| return view; | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Batch Message Mutation | |
| // | |
| // If you want to modify a single message, you can use the methods on it | |
| // directly. | |
| // | |
| // All actions are undoable and return an `UndoableOperation`. | |
| /** | |
| * Trash the given messages/conversations by moving them to the trash folder. | |
| * A trash folder will be created if one does not already exist. If the | |
| * message is already in the trash folder it will instead be immediately | |
| * removed. | |
| * | |
| * @param {MailMessage[]|MailConversation[]} arrayOfStuff | |
| * The messages or conversations to delete. This should be a homogenous | |
| * list, although in the future we could enhance things to support a | |
| * mixture. | |
| * @param {"last"|null} [opts.messageSelector] | |
| * Allows filtering the set of affected messages in a conversation when | |
| * conversations are provided. This would be crazy to use for `trash`, but | |
| * is mentioned here because if you provide it, we will use it. | |
| * @return {UndoableOperation} | |
| * An undoable operation that roughly describes what was done (to | |
| * facilitate describing the thing that can be undone) and a means of | |
| * triggering the undo. Note that while the actual undo() behavior will | |
| * attempt to leave things in their original state (rather than inverting | |
| * the original request), the characterization of how many | |
| * messages/conversations were impacted will not reflect these smarts. | |
| * | |
| * An `undoableOp` event will also be emitted on the base MailAPI instance | |
| * if that simplifies your life. | |
| */ | |
| trash: function(arrayOfStuff, opts) { | |
| let { convSelectors, affectedType, affectedCount } = | |
| this._normalizeConversationSelectorArgs(arrayOfStuff, opts); | |
| return this._sendUndoableRequest( | |
| { | |
| operation: 'trash', | |
| affectedType, | |
| affectedCount, | |
| }, | |
| { | |
| type: 'trash', | |
| conversations: convSelectors | |
| }); | |
| }, | |
| /** | |
| * Move the given messages/conversations to the desired target folder. Note | |
| * that there may be more appropriate semantic options you can take than a | |
| * direct move. For example: | |
| * - use: `trash()` to delete stuff. | |
| * - future: `archive()` to archive stuff. | |
| * | |
| * @param {MailMessage[]|MailConversation[]} arrayOfStuff | |
| * The messages or conversations to modify. This should be a homogenous | |
| * list, although in the future we could enhance things to support a | |
| * mixture. | |
| * @param {MailFolder} targetFolder | |
| * The folder to move the stuff to. The folder must belong to the same | |
| * account as the stuff. In the future we may also support an alternate | |
| * mechanism where a folder type rather than a specific folder can be | |
| * specified, in which case this would work across accounts. Please feel | |
| * free to raise an issue to discuss while also considering whether the | |
| * need actually merits a higher level operation. (Like 'trash' really | |
| * does want to be its own high-level thing and not just a re-branded | |
| * move operation.) | |
| * @param {"last"|null} [opts.messageSelector] | |
| * Allows filtering the set of affected messages in a conversation when | |
| * conversations are provided. | |
| * @return {UndoableOperation} | |
| * An undoable operation that roughly describes what was done (to | |
| * facilitate describing the thing that can be undone) and a means of | |
| * triggering the undo. Note that while the actual undo() behavior will | |
| * attempt to leave things in their original state (rather than inverting | |
| * the original request), the characterization of how many | |
| * messages/conversations were impacted will not reflect these smarts. | |
| * | |
| * An `undoableOp` event will also be emitted on the base MailAPI instance | |
| * if that simplifies your life. | |
| */ | |
| move: function(arrayOfStuff, targetFolder, opts) { | |
| let { convSelectors, affectedType, affectedCount } = | |
| this._normalizeConversationSelectorArgs(arrayOfStuff, opts); | |
| return this._sendUndoableRequest( | |
| { | |
| operation: 'move', | |
| affectedType, | |
| affectedCount, | |
| }, | |
| { | |
| type: 'move', | |
| conversations: convSelectors, | |
| targetFolderId: targetFolder.id | |
| }); | |
| }, | |
| /** | |
| * Mark the given conversations/messages as read/unread. | |
| * | |
| * @param {MailMessage[]|MailConversation[]} arrayOfStuff | |
| * The messages or conversations to modify. This should be a homogenous | |
| * list, although in the future we could enhance things to support a | |
| * mixture. | |
| * @param {Boolean} beRead | |
| * true to mark stuff read, false to mark stuff unread | |
| * @param {"last"|null} [opts.messageSelector] | |
| * Allows filtering the set of affected messages in a conversation when | |
| * conversations are provided. | |
| * @return {UndoableOperation} | |
| * An undoable operation that roughly describes what was done (to | |
| * facilitate describing the thing that can be undone) and a means of | |
| * triggering the undo. Note that while the actual undo() behavior will | |
| * attempt to leave things in their original state (rather than inverting | |
| * the original request), the characterization of how many | |
| * messages/conversations were impacted will not reflect these smarts. | |
| * | |
| * An `undoableOp` event will also be emitted on the base MailAPI instance | |
| * if that simplifies your life. | |
| */ | |
| markRead: function(arrayOfStuff, beRead) { | |
| return this.modifyTags( | |
| arrayOfStuff, | |
| { | |
| operation: beRead ? 'read' : 'unread', | |
| addTags: beRead ? ['\\Seen'] : null, | |
| removeTags: beRead ? null : ['\\Seen'] | |
| } | |
| ); | |
| }, | |
| /** | |
| * Star/un-star the given conversations/messages. | |
| * | |
| * @param {MailMessage[]|MailConversation[]} arrayOfStuff | |
| * The messages or conversations to modify. This should be a homogenous | |
| * list, although in the future we could enhance things to support a | |
| * mixture. | |
| * @param {Boolean} beStarred | |
| * true to star the stuff, false to un-star them. | |
| * @return {UndoableOperation} | |
| * An undoable operation that roughly describes what was done (to | |
| * facilitate describing the thing that can be undone) and a means of | |
| * triggering the undo. Note that while the actual undo() behavior will | |
| * attempt to leave things in their original state (rather than inverting | |
| * the original request), the characterization of how many | |
| * messages/conversations were impacted will not reflect these smarts. | |
| * | |
| * An `undoableOp` event will also be emitted on the base MailAPI instance | |
| * if that simplifies your life. | |
| */ | |
| markStarred: function(arrayOfStuff, beStarred) { | |
| return this.modifyTags( | |
| arrayOfStuff, | |
| { | |
| operation: beStarred ? 'star' : 'unstar', | |
| addTags: beStarred ? ['\\Flagged'] : null, | |
| removeTags: beStarred ? null : ['\\Flagged'], | |
| // If we're starring, we use the same heuristics setStarred used on | |
| // MailConversation, which is to only star the last one. This is | |
| // consistent with what gmail and friends do. Note that it is our | |
| // intent that this only applies to the conversation case, and at least | |
| // for the current implementation (as of writing this), this will not | |
| // be propagated in the messages case. | |
| messageSelector: beStarred ? 'last' : null | |
| } | |
| ); | |
| }, | |
| /** | |
| * Add/remove labels on the given conversations/messages. | |
| * | |
| * @param {MailMessage[]|MailConversation[]} arrayOfStuff | |
| * The messages or conversations to modify. This should be a homogenous | |
| * list, although in the future we could enhance things to support a | |
| * mixture. | |
| * @param {MailFolder[]} [opts.addLabels] | |
| * @param {MailFolder[]} [opts.removeLabels] | |
| * @param {"last"|null} [opts.messageSelector] | |
| * Allows filtering the set of affected messages in a conversation when | |
| * conversations are provided. | |
| * @return {UndoableOperation} | |
| * An undoable operation that roughly describes what was done (to | |
| * facilitate describing the thing that can be undone) and a means of | |
| * triggering the undo. Note that while the actual undo() behavior will | |
| * attempt to leave things in their original state (rather than inverting | |
| * the original request), the characterization of how many | |
| * messages/conversations were impacted will not reflect these smarts. | |
| * | |
| * An `undoableOp` event will also be emitted on the base MailAPI instance | |
| * if that simplifies your life. | |
| */ | |
| modifyLabels: function(arrayOfStuff, opts) { | |
| let { convSelectors, affectedType, affectedCount } = | |
| this._normalizeConversationSelectorArgs(arrayOfStuff, opts); | |
| return this._sendUndoableRequest( | |
| { | |
| operation: opts.operation || 'modifylabels', | |
| affectedType, | |
| affectedCount, | |
| }, | |
| { | |
| type: 'store_labels', | |
| conversations: convSelectors, | |
| add: normalizeFoldersToIds(opts.addLabels), | |
| remove: normalizeFoldersToIds(opts.removeLabels) | |
| }); | |
| }, | |
| /** | |
| * Add/remove labels on the given conversations/messages. | |
| * | |
| * @param {MailMessage[]|MailConversation[]} arrayOfStuff | |
| * The messages or conversations to modify. This should be a homogenous | |
| * list, although in the future we could enhance things to support a | |
| * mixture. | |
| * @param {String[]]} [opts.addTags] | |
| * @param {String[]} [opts.removeTags] | |
| * @param {"last"|null} [opts.messageSelector] | |
| * Allows filtering the set of affected messages in a conversation when | |
| * conversations are provided. | |
| * @return {UndoableOperation} | |
| * An undoable operation that roughly describes what was done (to | |
| * facilitate describing the thing that can be undone) and a means of | |
| * triggering the undo. Note that while the actual undo() behavior will | |
| * attempt to leave things in their original state (rather than inverting | |
| * the original request), the characterization of how many | |
| * messages/conversations were impacted will not reflect these smarts. | |
| * | |
| * An `undoableOp` event will also be emitted on the base MailAPI instance | |
| * if that simplifies your life. | |
| */ | |
| modifyTags: function(arrayOfStuff, opts) { | |
| let { convSelectors, affectedType, affectedCount } = | |
| this._normalizeConversationSelectorArgs(arrayOfStuff, opts); | |
| return this._sendUndoableRequest( | |
| { | |
| operation: opts.operation || 'modifytags', | |
| affectedType, | |
| affectedCount, | |
| }, | |
| { | |
| type: 'store_flags', | |
| conversations: convSelectors, | |
| add: opts.addTags, | |
| remove: opts.removeTags | |
| }); | |
| }, | |
| /** | |
| * Enable or disable outbox syncing for this account. This is | |
| * generally a temporary measure, used when the user is actively | |
| * editing the list of outbox messages and we don't want to | |
| * inadvertently move something out from under them. This change | |
| * does _not_ persist; it's meant to be used only for brief periods | |
| * of time, not as a "sync schedule" coordinator. | |
| */ | |
| setOutboxSyncEnabled: function (account, enabled) { | |
| return this._sendPromisedRequest({ | |
| type: 'outboxSetPaused', | |
| accountId: account.id, | |
| bePaused: !enabled | |
| }); // (the bridge sends null for the data, which is what gets resolved) | |
| }, | |
| /** | |
| * Parse a structured email address | |
| * into a display name and email address parts. | |
| * It will return null on a parse failure. | |
| * | |
| * @param {String} email A email address. | |
| * @return {Object} An object of the form { name, address }. | |
| */ | |
| parseMailbox: function(email) { | |
| try { | |
| var mailbox = addressparser.parse(email); | |
| return (mailbox.length >= 1) ? mailbox[0] : null; | |
| } | |
| catch (ex) { | |
| return null; | |
| } | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Contact Support | |
| resolveEmailAddressToPeep: function(emailAddress, callback) { | |
| var peep = ContactCache.resolvePeep({ name: null, address: emailAddress }); | |
| if (ContactCache.pendingLookupCount) { | |
| ContactCache.callbacks.push(callback.bind(null, peep)); | |
| } else { | |
| callback(peep); | |
| } | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Message Composition | |
| /** | |
| * Begin the message composition process, creating a MessageComposition that | |
| * stores the current message state and periodically persists its state to the | |
| * backend so that the message is potentially available to other clients and | |
| * recoverable in the event of a local crash. | |
| * | |
| * Composition is triggered in the context of a given message and folder so | |
| * that the correct account and sender identity for composition can be | |
| * inferred. Message may be null if there are no messages in the folder. | |
| * Folder is not required if a message is provided. | |
| * | |
| * @param {MailMessage} message | |
| * @param {MailFolder} folder | |
| * @param {Object} options | |
| * @param {'blank'|'reply'|'forward'} options.command | |
| * @param {'sender'|'all'} options.mode | |
| * The reply mode. This will eventually indicate the forwarding mode too. | |
| * @param {Boolean} [options.noComposer=false] | |
| * Don't actually want the MessageComposition instance created for you? | |
| * Pass true for this. You can always call resumeMessageComposition | |
| * yourself; that's all we do anyways. | |
| * @return {Promise<MessageComposition>} | |
| * A MessageComposition instance populated for use. You need to call | |
| * release on it when you are done. | |
| */ | |
| beginMessageComposition: function(message, folder, options) { | |
| if (!options) { | |
| options = {}; | |
| } | |
| return this._sendPromisedRequest({ | |
| type: 'createDraft', | |
| draftType: options.command, | |
| mode: options.mode, | |
| refMessageId: message ? message.id : null, | |
| refMessageDate: message ? message.date.valueOf() : null, | |
| folderId: folder ? folder.id : null | |
| }).then((data) => { | |
| let namer = { id: data.messageId, date: data.messageDate }; | |
| if (options.noComposer) { | |
| return namer; | |
| } else { | |
| return this.resumeMessageComposition(namer); | |
| } | |
| }); | |
| }, | |
| /** | |
| * Open a message as if it were a draft message (hopefully it is), returning | |
| * a Promise that will be resolved with a fully valid MessageComposition | |
| * object. You will need to call release | |
| * | |
| * @param {MailMessage|MessageObjNamer} namer | |
| */ | |
| resumeMessageComposition: function(namer) { | |
| return this.getMessage([namer.id, namer.date.valueOf()]).then((msg) => { | |
| let composer = new MessageComposition(this); | |
| return composer.__asyncInitFromMessage(msg); | |
| }); | |
| }, | |
| _composeAttach: function(messageId, attachmentDef) { | |
| this.__bridgeSend({ | |
| type: 'attachBlobToDraft', | |
| messageId, | |
| attachmentDef | |
| }); | |
| }, | |
| _composeDetach: function(messageId, attachmentRelId) { | |
| this.__bridgeSend({ | |
| type: 'detachAttachmentFromDraft', | |
| messageId, | |
| attachmentRelId | |
| }); | |
| }, | |
| _composeDone: function(messageId, command, draftFields) { | |
| return this._sendPromisedRequest({ | |
| type: 'doneCompose', | |
| messageId, command, draftFields | |
| }); | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // mode setting for back end universe. Set interactive | |
| // if the user has been exposed to the UI and it is a | |
| // longer lived application, not just a cron sync. | |
| setInteractive: function() { | |
| this.__bridgeSend({ | |
| type: 'setInteractive' | |
| }); | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Localization | |
| /** | |
| * Provide a list of localized strings for use in message composition. This | |
| * should be a dictionary with the following values, with their expected | |
| * default values for English provided. Try to avoid being clever and instead | |
| * just pick the same strings Thunderbird uses for these for the given locale. | |
| * | |
| * - wrote: "{{name}} wrote". Used for the lead-in to the quoted message. | |
| * - originalMessage: "Original Message". Gets put between a bunch of dashes | |
| * when forwarding a message inline. | |
| * - forwardHeaderLabels: | |
| * - subject | |
| * - date | |
| * - from | |
| * - replyTo (for the "reply-to" header) | |
| * - to | |
| * - cc | |
| */ | |
| useLocalizedStrings: function(strings) { | |
| this.__bridgeSend({ | |
| type: 'localizedStrings', | |
| strings: strings | |
| }); | |
| if (strings.folderNames) { | |
| this.l10n_folder_names = strings.folderNames; | |
| } | |
| }, | |
| /** | |
| * L10n strings for folder names. These map folder types to appropriate | |
| * localized strings. | |
| * | |
| * We don't remap unknown types, so this doesn't need defaults. | |
| */ | |
| l10n_folder_names: {}, | |
| l10n_folder_name: function(name, type) { | |
| if (this.l10n_folder_names.hasOwnProperty(type)) { | |
| var lowerName = name.toLowerCase(); | |
| // Many of the names are the same as the type, but not all. | |
| if ((type === lowerName) || | |
| (type === 'drafts') || | |
| (type === 'junk') || | |
| (type === 'queue')) { | |
| return this.l10n_folder_names[type]; | |
| } | |
| } | |
| return name; | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Configuration | |
| /** | |
| * Change one-or-more backend-wide settings; use `MailAccount.modifyAccount` | |
| * to chang per-account settings. | |
| */ | |
| modifyConfig: function(mods) { | |
| for (var key in mods) { | |
| if (LEGAL_CONFIG_KEYS.indexOf(key) === -1) { | |
| throw new Error(key + ' is not a legal config key!'); | |
| } | |
| } | |
| return this._sendPromisedRequest({ | |
| type: 'modifyConfig', | |
| mods | |
| }).then(() => null); | |
| }, | |
| _recv_config: function(msg) { | |
| this.config = msg.config; | |
| }, | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Diagnostics / Test Hacks | |
| /** | |
| * After a zero timeout, send a 'ping' to the bridge which will send a | |
| * 'pong' back, notifying the provided callback. This is intended to be hack | |
| * to provide a way to ensure that some function only runs after all of the | |
| * notifications have been received and processed by the back-end. | |
| * | |
| * Note that ping messages are always processed as they are received; they do | |
| * not get deferred like other messages. | |
| */ | |
| ping: function(callback) { | |
| var handle = this._nextHandle++; | |
| this._pendingRequests[handle] = { | |
| type: 'ping', | |
| callback: callback, | |
| }; | |
| // With the introduction of slice batching, we now wait to send the ping. | |
| // This is reasonable because there are conceivable situations where the | |
| // caller really wants to wait until all related callbacks fire before | |
| // dispatching. And the ping method is already a hack to ensure correctness | |
| // ordering that should be done using better/more specific methods, so this | |
| // change is not any less of a hack/evil, although it does cause misuse to | |
| // potentially be more capable of causing intermittent failures. | |
| window.setTimeout(() => { | |
| this.__bridgeSend({ | |
| type: 'ping', | |
| handle: handle, | |
| }); | |
| }, 0); | |
| }, | |
| _recv_pong: function(msg) { | |
| var req = this._pendingRequests[msg.handle]; | |
| delete this._pendingRequests[msg.handle]; | |
| req.callback(); | |
| }, | |
| /** | |
| * Legacy means of setting the debug logging level. Probably wants to go away | |
| * in favor of just using modifyConfig directly. Other debugging-y stuff | |
| * probably will operate similarly or get its own explicit API calls. | |
| */ | |
| debugSupport: function(command, argument) { | |
| if (command === 'setLogging') { | |
| this.config.debugLogging = argument; | |
| return this.modifyConfig({ | |
| debugLogging: argument | |
| }); | |
| } else if (command === 'dumpLog') { | |
| throw new Error('XXX circular logging currently not implemented'); | |
| } | |
| }, | |
| /** | |
| * Clear the set of new messages associated with the given account. Also | |
| * exposed on MailAccount as clearNewTracking. | |
| */ | |
| clearNewTrackingForAccount: function({ account, accountId, silent }) { | |
| if (account && !accountId) { | |
| accountId = account.id; | |
| } | |
| this.__bridgeSend({ | |
| type: 'clearNewTrackingForAccount', | |
| accountId, | |
| silent | |
| }); | |
| }, | |
| /** | |
| * Cause the 'newMessagesUpdate' message to be re-derived and re-broadcast. | |
| * This should only be used in exceptional circumstances because the whole | |
| * implementation of this assumes that persistent notifications are generated | |
| * by the broadcast. Since the message will also automatically be sent when | |
| * the set of new messages changes, if you are calling this, you are by | |
| * definition asking for redundant data you should already have heard about. | |
| * I would prefix this with `debug` but it's possible there's a reason to | |
| * expose this that's not horrible. | |
| */ | |
| flushNewAggregates: function() { | |
| this.__bridgeSend({ | |
| type: 'flushNewAggregates' | |
| }); | |
| }, | |
| /** | |
| * Compel the backend to act like it received a cronsync. | |
| * | |
| * @param {AccountId[]} [arg.accountIds] | |
| * The list of account ids to act like we are being told to sync. If | |
| * omitted, the list of all accounts is used. | |
| * @param {AccountId[]} [arg.notificationAccountIds] | |
| * The list of account ids to act like we have outstanding notifications for | |
| * (so as to not trigger a new_tracking status clearing). If omitted, the | |
| * list of all accounts is used. | |
| */ | |
| debugForceCronSync: function({ accountIds, notificationAccountIds }) { | |
| let allAccountIds = this.accounts.items.map(account => account.id); | |
| if (!accountIds) { | |
| accountIds = allAccountIds; | |
| } | |
| if (!notificationAccountIds) { | |
| notificationAccountIds = allAccountIds; | |
| } | |
| this.__bridgeSend({ | |
| type: 'debugForceCronSync', | |
| accountIds, | |
| notificationAccountIds | |
| }); | |
| }, | |
| /** | |
| * Retrieve the persisted-to-disk log entries we create for things like | |
| * cronsync. | |
| * | |
| * @return {Promise<Object[]>} | |
| */ | |
| getPersistedLogs: function() { | |
| return this._sendPromisedRequest({ | |
| type: 'getPersistedLogs' | |
| }); | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| }); | |
| }); // end define |