Beispiel #1
0
define(function(require) {
  var appSelf = require('app_self'),
      evt = require('evt'),
      queryString = require('query_string'),
      queryURI = require('query_uri');

  var pending = {};
  // htmlCacheRestorePendingMessage defined in html_cache_restore,
  // see comment for it.
  var cachedList = (window.htmlCacheRestorePendingMessage &&
                    window.htmlCacheRestorePendingMessage.length) ?
      window.htmlCacheRestorePendingMessage : [];
  // Convert the cached list to named properties on pending.
  cachedList.forEach(function(type) {
    pending[type] = true;
  });

  var appMessages = evt.mix({
    /**
     * Whether or not we have pending messages.
     *
     * @param {string} type message type.
     * @return {boolean} Whether or there are pending message(s) of the type.
     */
    hasPending: function(type) {
      return pending.hasOwnProperty(type) ||
             (navigator.mozHasPendingMessage &&
              navigator.mozHasPendingMessage(type));
    },

    /**
     * Perform requested activity.
     *
     * @param {MozActivityRequestHandler} req activity invocation.
     */
    onActivityRequest: function(req) {
      // Parse the activity request.
      var source = req.source;
      var sourceData = source.data;
      var activityName = source.name;
      var dataType = sourceData.type;
      var url = sourceData.url || sourceData.URI;

      // To assist in bug analysis, log the start of the activity here.
      console.log('Received activity: ' + activityName);

      // Dynamically load util, since it is only needed for certain
      // pathways in this module.
      require(['attachment_name'], function(attachmentName) {
        var data = {};
        if (dataType === 'url' && activityName === 'share') {
          data.body = url;
        } else {
          var urlParts = url ? queryURI(url) : [];
          data.to = urlParts[0];
          data.subject = urlParts[1];
          data.body = typeof urlParts[2] === 'string' ? urlParts[2] : null;
          data.cc = urlParts[3];
          data.bcc = urlParts[4];
          data.attachmentBlobs = sourceData.blobs || [];
          data.attachmentNames = sourceData.filenames || [];

          attachmentName.ensureNameList(data.attachmentBlobs,
                                     data.attachmentNames);
        }

        this.emitWhenListener('activity', activityName, data, req);
      }.bind(this));
    },

    onNotification: function(msg) {
      // Skip notification events that are not from a notification
      // "click". The system app will also notify this method of
      // any close events for notificaitons, which are not at all
      // interesting, at least for the purposes here.
      if (!msg.clicked)
        return;

      // Need to manually get all notifications and close the one
      // that triggered this event due to fallout from 890440 and
      // 966481.
      if (typeof Notification !== 'undefined' && Notification.get) {
        Notification.get().then(function(notifications) {
          if (notifications) {
            notifications.some(function(notification) {
              // Compare tags, as the tag is based on the account ID and
              // we only have one notification per account. Plus, there
              // is no "id" field on the notification.
              if (notification.tag === msg.tag && notification.close) {
                notification.close();
                return true;
              }
            });
          }
        });
      }

      // icon url parsing is a cray cray way to pass day day
      var data = queryString.toObject((msg.imageURL || '').split('#')[1]);

      if (document.hidden) {
        appSelf.latest('self', function(app) {
          app.launch();
        });
      }

      this.emitWhenListener('notification', data);
    }
  });

  if ('mozSetMessageHandler' in navigator) {
    navigator.mozSetMessageHandler(
        'activity', appMessages.onActivityRequest.bind(appMessages));
    navigator.mozSetMessageHandler(
        'notification', appMessages.onNotification.bind(appMessages));
    evt.on(
        'notification', appMessages.onNotification.bind(appMessages));

    // Do not listen for navigator.mozSetMessageHandler('alarm') type, that is
    // only done in the back end's cronsync for now.
  } else {
    console.warn('Activity support disabled!');
  }

  return appMessages;
});
Beispiel #2
0
define('mail_app', function(require, exports, module) {

var appMessages = require('app_messages'),
    htmlCache = require('html_cache'),
    mozL10n = require('l10n!'),
    common = require('mail_common'),
    evt = require('evt'),
    model = require('model'),
    headerCursor = require('header_cursor').cursor,
    Cards = common.Cards,
    waitingForCreateAccountPrompt = false,
    activityCallback = null;

require('sync');
require('wake_locks');

model.latestOnce('api', function(api) {
  // If our password is bad, we need to pop up a card to ask for the updated
  // password.
  api.onbadlogin = function(account, problem, whichSide) {
    switch (problem) {
      case 'bad-user-or-pass':
        Cards.pushCard('setup_fix_password', 'default', 'animate',
                  { account: account,
                    whichSide: whichSide,
                    restoreCard: Cards.activeCardIndex },
                  'right');
        break;
      case 'imap-disabled':
      case 'pop3-disabled':
        Cards.pushCard('setup_fix_gmail', 'default', 'animate',
                  { account: account, restoreCard: Cards.activeCardIndex },
                  'right');
        break;
      case 'needs-app-pass':
        Cards.pushCard('setup_fix_gmail_twofactor', 'default', 'animate',
                  { account: account, restoreCard: Cards.activeCardIndex },
                  'right');
        break;
    }
  };

  api.useLocalizedStrings({
    wrote: mozL10n.get('reply-quoting-wrote'),
    originalMessage: mozL10n.get('forward-original-message'),
    forwardHeaderLabels: {
      subject: mozL10n.get('forward-header-subject'),
      date: mozL10n.get('forward-header-date'),
      from: mozL10n.get('forward-header-from'),
      replyTo: mozL10n.get('forward-header-reply-to'),
      to: mozL10n.get('forward-header-to'),
      cc: mozL10n.get('forward-header-cc')
    },
    folderNames: {
      inbox: mozL10n.get('folder-inbox'),
      sent: mozL10n.get('folder-sent'),
      drafts: mozL10n.get('folder-drafts'),
      trash: mozL10n.get('folder-trash'),
      queue: mozL10n.get('folder-queue'),
      junk: mozL10n.get('folder-junk'),
      archives: mozL10n.get('folder-archives'),
      localdrafts: mozL10n.get('folder-localdrafts')
    }
  });
});

// Handle cases where a default card is needed for back navigation
// after a non-default entry point (like an activity) is triggered.
Cards.pushDefaultCard = function(onPushed) {
  model.latestOnce('foldersSlice', function() {
    Cards.pushCard('message_list', 'nonsearch', 'none', {
      onPushed: onPushed
    },
    // Default to "before" placement.
    'left');
  });
};

Cards._init();

var finalCardStateCallback,
    waitForAppMessage = false,
    startedInBackground = false,
    cachedNode = Cards._cardsNode.children[0],
    startCardId = cachedNode && cachedNode.getAttribute('data-type');

var startCardArgs = {
  'setup_account_info': [
    'setup_account_info', 'default', 'immediate',
    {
      onPushed: function(impl) {
        htmlCache.delayedSaveFromNode(impl.domNode.cloneNode(true));
      }
    }
  ],
  'message_list': [
    'message_list', 'nonsearch', 'immediate', {}
  ]
};

function pushStartCard(id, addedArgs) {
  var args = startCardArgs[id];
  if (!args) {
    throw new Error('Invalid start card: ' + id);
  }

  //Add in cached node to use (could be null)
  args[3].cachedNode = cachedNode;

  // Mix in addedArgs to the args object that is passed to pushCard.
  if (addedArgs) {
    Object.keys(addedArgs).forEach(function(key) {
      args[3][key] = addedArgs[key];
    });
  }

  return Cards.pushCard.apply(Cards, args);
}

if (appMessages.hasPending('activity') ||
    appMessages.hasPending('notification')) {
  // There is an activity, do not use the cache node, start fresh,
  // and block normal first card selection, wait for activity.
  cachedNode = null;
  waitForAppMessage = true;
}

if (appMessages.hasPending('alarm')) {
  // There is an alarm, do not use the cache node, start fresh,
  // as we were woken up just for the alarm.
  cachedNode = null;
  startedInBackground = true;
}

// If still have a cached node, then show it.
if (cachedNode) {
  // Wire up a card implementation to the cached node.
  if (startCardId) {
    pushStartCard(startCardId);
  } else {
    cachedNode = null;
  }
}

/**
 * When determination of real start state is known after
 * getting data, then make sure the correct card is
 * shown. If the card used from cache is not correct,
 * wipe out the cards and start fresh.
 * @param  {String} cardId the desired card ID.
 */
function resetCards(cardId, args) {
  cachedNode = null;

  var startArgs = startCardArgs[cardId],
      query = [startArgs[0], startArgs[1]];

  if (!Cards.hasCard(query)) {
    Cards.removeAllCards();
    pushStartCard(cardId, args);
  }
}

/*
 * Determines if current card is a nonsearch message_list
 * card, which is the default kind of card.
 */
function isCurrentCardMessageList() {
  var cardType = Cards.getCurrentCardType();
  return (cardType &&
          cardType[0] === 'message_list' &&
          cardType[1] === 'nonsearch');
}

/**
 * Tracks what final card state should be shown. If the
 * app started up hidden for a cronsync, do not actually
 * show the UI until the app becomes visible, so that
 * extra work can be avoided for the hidden cronsync case.
 */
function showFinalCardState(fn) {
  if (startedInBackground && document.hidden) {
    finalCardStateCallback = fn;
  } else {
    fn();
  }
}

/**
 * Shows the message list. Assumes that the correct
 * account and inbox have already been selected.
 */
function showMessageList(args) {
  showFinalCardState(function() {
    resetCards('message_list', args);
  });
}

// Handles visibility changes: if the app becomes visible
// being hidden via a cronsync startup, trigger UI creation.
document.addEventListener('visibilitychange', function onVisibilityChange() {
  if (startedInBackground && finalCardStateCallback && !document.hidden) {
    finalCardStateCallback();
    finalCardStateCallback = null;
  }
}, false);

// Some event modifications during setup do not have full account
// IDs. This listener catches those modifications and applies
// them when the data is available.
evt.on('accountModified', function(accountId, data) {
  model.latestOnce('acctsSlice', function() {
    var account = model.getAccount(accountId);
    if (account) {
      account.modifyAccount(data);
    }
  });
});

// The add account UI flow is requested.
evt.on('addAccount', function() {
  Cards.removeAllCards();

  // Show the first setup card again.
  pushStartCard('setup_account_info', {
    allowBack: true
  });
});

function resetApp() {
  // Clear any existing local state and reset UI/model state.
  waitForAppMessage = false;
  waitingForCreateAccountPrompt = false;
  activityCallback = null;

  Cards.removeAllCards();
  model.init();
}

function activityContinued() {
  if (activityCallback) {
    var activityCb = activityCallback;
    activityCallback = null;
    activityCb();
    return true;
  }
  return false;
}

// An account was deleted. Burn it all to the ground and
// rise like a phoenix. Prefer a UI event vs. a slice
// listen to give flexibility about UI construction:
// an acctsSlice splice change may not warrant removing
// all the cards.
evt.on('accountDeleted', resetApp);
evt.on('resetApp', resetApp);

// A request to show the latest account in the UI.
// Usually triggered after an account has been added.
evt.on('showLatestAccount', function() {
  Cards.removeAllCards();

  model.latestOnce('acctsSlice', function(acctsSlice) {
    var account = acctsSlice.items[acctsSlice.items.length - 1];

    model.changeAccount(account, function() {
      pushStartCard('message_list', {
        // If waiting to complete an activity, do so after pushing the
        // message list card.
        onPushed: activityContinued
      });
    });
  });
});

model.on('acctsSlice', function() {
  if (!model.hasAccount()) {
    if (!waitingForCreateAccountPrompt) {
      resetCards('setup_account_info');
    }
  } else {
    model.latestOnce('foldersSlice', function() {
      if (waitForAppMessage) {
        return;
      }

      // If an activity was waiting for an account, trigger it now.
      if (activityContinued()) {
        return;
      }

      showMessageList();
    });
  }
});

var lastActivityTime = 0;
appMessages.on('activity', function(type, data, rawActivity) {
  // Rate limit rapid fire activity triggers, like an accidental
  // double tap. While the card code adjusts for the taps, in
  // the case of configured account, user can end up with multiple
  // compose cards in the stack, which is probably confusing,
  // and the rapid tapping is likely just an accident, or an
  // incorrect user belief that double taps are needed for
  // activation.
  var activityTime = Date.now();
  if (activityTime < lastActivityTime + 1000) {
    return;
  }
  lastActivityTime = activityTime;

  function initComposer() {
    Cards.pushCard('compose', 'default', 'immediate', {
      activity: rawActivity,
      composerData: {
        onComposer: function(composer) {
          var attachmentBlobs = data.attachmentBlobs;
          /* to/cc/bcc/subject/body all have default values that shouldn't
          be clobbered if they are not specified in the URI*/
          if (data.to) {
            composer.to = data.to;
          }
          if (data.subject) {
            composer.subject = data.subject;
          }
          if (data.body) {
            composer.body = { text: data.body };
          }
          if (data.cc) {
            composer.cc = data.cc;
          }
          if (data.bcc) {
            composer.bcc = data.bcc;
          }
          if (attachmentBlobs) {
            for (var iBlob = 0; iBlob < attachmentBlobs.length; iBlob++) {
              composer.addAttachment({
                name: data.attachmentNames[iBlob],
                blob: attachmentBlobs[iBlob]
              });
            }
          }
        }
      }
    });
  }

  function promptEmptyAccount() {
    common.ConfirmDialog.show(mozL10n.get('setup-empty-account-prompt'),
    function(confirmed) {
      if (!confirmed) {
        rawActivity.postError('cancelled');
      }

      waitingForCreateAccountPrompt = false;

      // No longer need to wait for the activity to complete, it needs
      // normal card flow
      waitForAppMessage = false;

      activityCallback = initComposer;

      // Always just reset to setup account in case the system does
      // not properly close out the email app on a cancelled activity.
      resetCards('setup_account_info');
    });
  }

  // Remove previous cards because the card stack could get
  // weird if inserting a new card that would not normally be
  // at that stack level. Primary concern: going to settings,
  // then trying to add a compose card at that stack level.
  // More importantly, the added card could have a "back"
  // operation that does not mean "back to previous state",
  // but "back in application flowchart". Message list is a
  // good known jump point, so do not needlessly wipe that one
  // out if it is the current one. Message list is a good
  // known jump point, so do not needlessly wipe that one out
  // if it is the current one.
  if (!isCurrentCardMessageList()) {
    Cards.removeAllCards();
  }

  if (model.inited) {
    if (model.hasAccount()) {
      initComposer();
    } else {
      waitingForCreateAccountPrompt = true;
      promptEmptyAccount();
    }
  } else {
    // Be optimistic and start rendering compose as soon as possible
    // In the edge case that email is not configured, then the empty
    // account prompt will be triggered quickly in the next section.
    initComposer();

    waitingForCreateAccountPrompt = true;
    model.latestOnce('acctsSlice', function activityOnAccount() {
      if (!model.hasAccount()) {
        promptEmptyAccount();
      }
    });
  }
});

appMessages.on('notification', function(data) {
  var type = data ? data.type : '';

  model.latestOnce('foldersSlice', function latestFolderSlice() {
    function onCorrectFolder() {
      function onPushed() {
        waitForAppMessage = false;
      }

      // Remove previous cards because the card stack could get
      // weird if inserting a new card that would not normally be
      // at that stack level. Primary concern: going to settings,
      // then trying to add a reader or message list card at that
      // stack level. More importantly, the added card could have
      // a "back" operation that does not mean "back to previous
      // state", but "back in application flowchart". Message
      // list is a good known jump point, so do not needlessly
      // wipe that one out if it is the current one.
      if (!isCurrentCardMessageList()) {
        Cards.removeAllCards();
      }

      if (type === 'message_list') {
        showMessageList({
          onPushed: onPushed
        });
      } else if (type === 'message_reader') {
        headerCursor.setCurrentMessageBySuid(data.messageSuid);

        Cards.pushCard(type, 'default', 'immediate', {
            messageSuid: data.messageSuid,
            onPushed: onPushed
        });
      } else {
        console.error('unhandled notification type: ' + type);
      }
    }

    var acctsSlice = model.acctsSlice,
        accountId = data.accountId;

    if (model.account.id === accountId) {
      return model.selectInbox(onCorrectFolder);
    } else {
      var newAccount;
      acctsSlice.items.some(function(account) {
        if (account.id === accountId) {
          newAccount = account;
          return true;
        }
      });

      if (newAccount) {
        model.changeAccount(newAccount, onCorrectFolder);
      }
    }
  });
});

model.init();
});
Beispiel #3
0
    createdCallback: function() {
      // Sync display
      this._needsSizeLastSync = true;
      this.updateLastSynced();

      this.curFolder = null;
      this.isIncomingFolder = true;

      // Binding "this" to some functions as they are used for event listeners.
      this._hideSearchBoxByScrolling =
                                      this._hideSearchBoxByScrolling.bind(this);
      this._folderChanged = this._folderChanged.bind(this);
      this.onNewMail = this.onNewMail.bind(this);
      this.onFoldersSliceChange = this.onFoldersSliceChange.bind(this);

      this.usingCachedNode = this.dataset.cached === 'cached';

      this.msgVScroll.on('messagesSpliceStart', function(index,
                                                         howMany,
                                                         addedItems,
                                                         requested,
                                                         moreExpected) {
        this._clearCachedMessages();
      }.bind(this));

      this.msgVScroll.on('messagesSpliceEnd', function(index,
                                                       howMany,
                                                       addedItems,
                                                       requested,
                                                       moreExpected) {
        // Only cache if it is an add or remove of items
        if (addedItems.length || howMany) {
          this._considerCacheDom(index);
        }
      }.bind(this));

      this.msgVScroll.on('messagesChange', function(message, index) {
        this.onMessagesChange(message, index);
      }.bind(this));

      this._emittedContentEvents = false;
      this.msgVScroll.on('messagesComplete', function(newEmailCount) {
        this.onNewMail(newEmailCount);

        // Inform that content is ready. There could actually be a small delay
        // with vScroll.updateDataBind from rendering the final display, but it
        // is small enough that it is not worth trying to break apart the design
        // to accommodate this metrics signal.
        if (!this._emittedContentEvents) {
          evt.emit('metrics:contentDone');
          this._emittedContentEvents = true;
        }
      }.bind(this));

      // Outbox has some special concerns, override status method to account for
      // it. Do this **before** initing the vscroll, so that the override is
      // used.
      var oldMessagesStatus = this.msgVScroll.messages_status;
      this.msgVScroll.messages_status = function(newStatus) {
        // The outbox's refresh button is used for sending messages, so we
        // ignore any syncing events generated by the slice. The outbox
        // doesn't need to show many of these indicators (like the "Load
        // More Messages..." node, etc.) and it has its own special
        // "refreshing" display, as documented elsewhere in this file.
        if (!this.curFolder || this.curFolder.type === 'outbox') {
          return;
        }

        return oldMessagesStatus.call(this.msgVScroll, newStatus);
      }.bind(this);

      this.msgVScroll.on('emptyLayoutShown', function() {
        this._clearCachedMessages();

        // The outbox can't refresh anything if there are no messages.
        if (this.curFolder.type === 'outbox') {
          this.refreshBtn.disabled = true;
        }

        this.editBtn.disabled = true;

        this._hideSearchBoxByScrolling();

      }.bind(this));
      this.msgVScroll.on('emptyLayoutHidden', function() {
        this.editBtn.disabled = false;
        this.refreshBtn.disabled = false;
      }.bind(this));

      this.msgVScroll.on('syncInProgress', function(syncInProgress) {
        if (syncInProgress) {
          this.setRefreshState(true);
        } else {
          this.setRefreshState(false);
          this._manuallyTriggeredSync = false;
        }
      }.bind(this));

      var vScrollBindData = (function bindNonSearch(model, node) {
          model.element = node;
          node.message = model;
          this.updateMessageDom(model);
        }).bind(this);
      this.msgVScroll.init(this.scrollContainer,
                           vScrollBindData,
                           defaultVScrollData);

      // Event listeners for VScroll events.
      this.msgVScroll.vScroll.on('inited', this._hideSearchBoxByScrolling);
      this.msgVScroll.vScroll.on('dataChanged', this._hideSearchBoxByScrolling);
      this.msgVScroll.vScroll.on('recalculated', function(calledFromTop) {
        if (calledFromTop) {
          this._hideSearchBoxByScrolling();
        }
      }.bind(this));

      this._topBar = new MessageListTopBar(
        this.querySelector('.message-list-topbar')
      );
      this._topBar.bindToElements(this.scrollContainer,
                                  this.msgVScroll.vScroll);

      this.onFolderPickerClosing = this.onFolderPickerClosing.bind(this);
      evt.on('folderPickerClosing', this.onFolderPickerClosing);
    },
Beispiel #4
0
define(function(require) {
  'use strict';
  var lockTimeouts = {},
      evt = require('evt'),
      allLocks = {},

      // Using an object instead of an array since dataIDs are unique
      // strings.
      dataOps = {},
      dataOpsTimeoutId = 0,

      // Only allow keeping the locks for a maximum of 45 seconds.
      // This is to prevent a long, problematic sync from consuming
      // all of the battery power in the phone. A more sophisticated
      // method may be to adjust the size of the timeout based on
      // past performance, but that would mean keeping a persistent
      // log of attempts. This naive approach just tries to catch the
      // most likely set of failures: just a temporary really bad
      // cell network situation that once the next sync happens, the
      // issue is resolved.
      maxLockInterval = 45000,

      // Allow UI-triggered data operations to complete in a wake lock timeout
      // case, but only for a certain amount of time, because they could be the
      // cause of the wake lock timeout.
      dataOpsTimeout = 5000;

  // START failsafe close support, bug 1025727.
  // If the wake locks are timed out, it means sync went on too long, and there
  // is likely a problem. Reset state via app shutdown. Allow for UI-triggered
  // data operations to complete though before finally releasing the wake locks
  // and shutting down.
  function close() {
    // Reset state in case a close does not actually happen.
    dataOps = {};
    dataOpsTimeoutId = 0;

    // Only really close if the app is hidden.
    if (document.hidden) {
      console.log('email: cronsync wake locks expired, force closing app');
      window.close();
    } else {
      console.log('email: cronsync wake locks expired, but app visible, ' +
                  'not force closing');
      // User is using the app. Just clear all locks so we do not burn battery.
      // This means the app could still be in a bad data sync state, so just
      // need to rely on the next sync attempt or OOM from other app usage.
      Object.keys(allLocks).forEach(function(accountKey) {
        clearLocks(accountKey);
      });
    }
  }

  function closeIfNoDataOps() {
    var dataOpsKeys = Object.keys(dataOps);

    if (!dataOpsKeys.length) {
      // All clear, no waiting data operations, shut it down.
      return close();
    }

    console.log('email: cronsync wake lock force shutdown waiting on email ' +
                'data operations: ' + dataOpsKeys.join(', '));
    // Allow data operations to complete, but also set a cap on that since
    // they could be the ones causing the sync to fail. Give it 5 seconds.
    dataOpsTimeoutId = setTimeout(close, dataOpsTimeout);
  }

  // Listen for data operation events that might want to delay the failsafe
  // close switch.
  evt.on('uiDataOperationStart', function(dataId) {
    dataOps[dataId] = true;
  });

  evt.on('uiDataOperationStop', function(dataId) {
    delete dataOps[dataId];

    if (dataOpsTimeoutId && !Object.keys(dataOps).length) {
      clearTimeout(dataOpsTimeoutId);
      close();
    }
  });
  // END failsafe close

  function clearLocks(accountKey) {
    console.log('email: clearing wake locks for "' + accountKey + '"');

    // Clear timer
    var lockTimeoutId = lockTimeouts[accountKey];
    if (lockTimeoutId) {
      clearTimeout(lockTimeoutId);
    }
    lockTimeouts[accountKey] = 0;

    // Clear the locks
    var locks = allLocks[accountKey];
    allLocks[accountKey] = null;
    if (locks) {
      locks.forEach(function(lock) {
        lock.unlock();
      });
    }
  }

  // Creates a string key from an array of string IDs. Uses a space
  // separator since that cannot show up in an ID.
  function makeAccountKey(accountIds) {
    return 'id' + accountIds.join(' ');
  }

  function onCronStop(accountIds) {
    clearLocks(makeAccountKey(accountIds));
  }

  evt.on('cronSyncWakeLocks', function(accountKey, locks) {
    if (lockTimeouts[accountKey]) {
      // Only support one set of locks. Better to err on the side of
      // saving the battery and not continue syncing vs supporting a
      // pathologic error that leads to a compound set of locks but
      // end up with more syncs completing.
      clearLocks(accountKey);
    }

    allLocks[accountKey] = locks;

    // If timeout is reached, means app is stuck in a bad state, and just
    // shut it down via the failsafe close.
    lockTimeouts[accountKey] = setTimeout(closeIfNoDataOps, maxLockInterval);
  });

  evt.on('cronSyncStop', onCronStop);
});
Beispiel #5
0
define(function(require) {

var appSelf = require('app_self'),
    evt = require('evt'),
    queryURI = require('query_uri'),
    queryString = require('query_string'),
    appMessages = evt.mix({}),
    pending = {},
    lastNotifyId = 0,
    // htmlCacheRestorePendingMessage defined in html_cache_restore,
    // see comment for it.
    cachedList = (window.htmlCacheRestorePendingMessage &&
              window.htmlCacheRestorePendingMessage.length) ?
              window.htmlCacheRestorePendingMessage : [];

// Convert the cached list to named properties on pending.
cachedList.forEach(function(type) {
  pending[type] = true;
});

appMessages.hasPending = function(type) {
  return pending.hasOwnProperty(type) || (navigator.mozHasPendingMessage &&
                                          navigator.mozHasPendingMessage(type));
};

function onNotification(msg) {

  console.log('email got notification: ' + JSON.stringify(msg, null, '  '));

  // icon url parsing is a cray cray way to pass day day
  var data = queryString.toObject((msg.imageURL || '').split('#')[1]);

  // Do not handle duplicate notifications. May be a bug in the
  // notifications system.
  if (data.notifyId && data.notifyId === lastNotifyId)
    return;
  lastNotifyId = data.notifyId;

  console.log('dispatching notification');

  if (document.hidden) {
    appSelf.latest('self', function(app) {
        app.launch();
    });
  }
  appMessages.emitWhenListener('notification', data);
}

if ('mozSetMessageHandler' in navigator) {
  navigator.mozSetMessageHandler('activity', function onActivity(message) {
    var activityName = message.source.name,
        attachmentBlobs = message.source.data.blobs,
        attachmentNames = message.source.data.filenames,
        url = message.source.data.url || message.source.data.URI,
        urlParts = url ? queryURI(url) : [];

    // To assist in bug analysis, log the start of the activity here.
    console.log('activity!', activityName);

    var composeData = {
      to: urlParts[0],
      subject: urlParts[1],
      body: typeof urlParts[2] === 'string' ? urlParts[2] : null,
      cc: urlParts[3],
      bcc: urlParts[4],
      attachmentBlobs: attachmentBlobs,
      attachmentNames: attachmentNames
    };

    appMessages.emitWhenListener('activity',
                                 activityName, composeData, message);
  });

  // Notifications can come from outside the app via the system,
  // by calling the mozSetMessageHandler listener. That happens
  // if the app is closed. If the app is open, then the app has
  // to listen for the onclick on the notification, and a
  // synthetic "event" is triggered via evt.
  navigator.mozSetMessageHandler('notification', onNotification);
  evt.on('notification', onNotification);

  // Do not listen for navigator.mozSetMessageHandler('alarm') type, that is
  // only done in the back end's cronsync for now.
}
else {
  console.warn('Activity support disabled!');
}

return appMessages;

});
Beispiel #6
0
define('mail_app', function(require, exports, module) {

var appMessages = require('app_messages'),
    htmlCache = require('html_cache'),
    mozL10n = require('l10n!'),
    cards = require('cards'),
    ConfirmDialog = require('confirm_dialog'),
    evt = require('evt'),
    model = require('model'),
    headerCursor = require('header_cursor').cursor,
    slice = Array.prototype.slice,
    waitingForCreateAccountPrompt = false,
    activityCallback = null;

require('shared/js/font_size_utils');
require('metrics');
require('sync');
require('wake_locks');

model.latestOnce('api', function(api) {
  // If our password is bad, we need to pop up a card to ask for the updated
  // password.
  api.onbadlogin = function(account, problem, whichSide) {
    switch (problem) {
      case 'bad-user-or-pass':
        cards.pushCard('setup_fix_password', 'animate',
                  { account: account,
                    whichSide: whichSide,
                    restoreCard: cards.activeCardIndex },
                  'right');
        break;
      case 'imap-disabled':
      case 'pop3-disabled':
        cards.pushCard('setup_fix_gmail', 'animate',
                  { account: account, restoreCard: cards.activeCardIndex },
                  'right');
        break;
      case 'needs-app-pass':
        cards.pushCard('setup_fix_gmail_twofactor', 'animate',
                  { account: account, restoreCard: cards.activeCardIndex },
                  'right');
        break;
      case 'needs-oauth-reauth':
        cards.pushCard('setup_fix_oauth2', 'animate',
                  { account: account, restoreCard: cards.activeCardIndex },
                  'right');
        break;
    }
  };

  api.useLocalizedStrings({
    wrote: mozL10n.get('reply-quoting-wrote'),
    originalMessage: mozL10n.get('forward-original-message'),
    forwardHeaderLabels: {
      subject: mozL10n.get('forward-header-subject'),
      date: mozL10n.get('forward-header-date'),
      from: mozL10n.get('forward-header-from'),
      replyTo: mozL10n.get('forward-header-reply-to'),
      to: mozL10n.get('forward-header-to'),
      cc: mozL10n.get('forward-header-cc')
    },
    folderNames: {
      inbox: mozL10n.get('folder-inbox'),
      outbox: mozL10n.get('folder-outbox'),
      sent: mozL10n.get('folder-sent'),
      drafts: mozL10n.get('folder-drafts'),
      trash: mozL10n.get('folder-trash'),
      queue: mozL10n.get('folder-queue'),
      junk: mozL10n.get('folder-junk'),
      archives: mozL10n.get('folder-archives'),
      localdrafts: mozL10n.get('folder-localdrafts')
    }
  });
});

// Handle cases where a default card is needed for back navigation
// after a non-default entry point (like an activity) is triggered.
cards.pushDefaultCard = function(onPushed) {
  model.latestOnce('foldersSlice', function() {
    cards.pushCard('message_list', 'none', {
      onPushed: onPushed
    },
    // Default to "before" placement.
    'left');
  });
};

cards._init();

var finalCardStateCallback,
    waitForAppMessage = false,
    startedInBackground = false,
    cachedNode = cards._cardsNode.children[0],
    startCardId = cachedNode && cachedNode.getAttribute('data-type');

function getStartCardArgs(id) {
  // Use a function that returns fresh arrays for each call so that object
  // in that last array position is fresh for each call and does not have other
  // properties mixed in to it by multiple reuse of the same object
  // (bug 1031588).
  if (id === 'setup_account_info') {
    return [
      'setup_account_info', 'immediate',
      {
        onPushed: function(cardNode) {
          var cachedNode =
                htmlCache.cloneAsInertNodeAvoidingCustomElementHorrors(
                  cardNode);
          cachedNode.dataset.cached = 'cached';
          htmlCache.delayedSaveFromNode(cachedNode);
        }
      }
    ];
  } else if (id === 'message_list') {
    return [
      'message_list', 'immediate', {}
    ];
  }
}

function pushStartCard(id, addedArgs) {
  var args = getStartCardArgs(id);
  if (!args) {
    throw new Error('Invalid start card: ' + id);
  }

  //Add in cached node to use (could be null)
  args[2].cachedNode = cachedNode;

  // Mix in addedArgs to the args object that is passed to pushCard.
  if (addedArgs) {
    Object.keys(addedArgs).forEach(function(key) {
      args[2][key] = addedArgs[key];
    });
  }

  return cards.pushCard.apply(cards, args);
}

if (appMessages.hasPending('activity') ||
    appMessages.hasPending('notification')) {
  // There is an activity, do not use the cache node, start fresh,
  // and block normal first card selection, wait for activity.
  cachedNode = null;
  waitForAppMessage = true;
  console.log('email waitForAppMessage');
}

if (appMessages.hasPending('alarm')) {
  // There is an alarm, do not use the cache node, start fresh,
  // as we were woken up just for the alarm.
  cachedNode = null;
  startedInBackground = true;
  console.log('email startedInBackground');
}

// If still have a cached node, then show it.
if (cachedNode) {
  // l10n may not see this as it was injected before l10n.js was loaded,
  // so let it know it needs to translate it.
  mozL10n.translateFragment(cachedNode);

  // Wire up a card implementation to the cached node.
  if (startCardId) {
    pushStartCard(startCardId);
  } else {
    cachedNode = null;
  }
}

/**
 * When determination of real start state is known after
 * getting data, then make sure the correct card is
 * shown. If the card used from cache is not correct,
 * wipe out the cards and start fresh.
 * @param  {String} cardId the desired card ID.
 */
function resetCards(cardId, args) {
  cachedNode = null;

  var startArgs = getStartCardArgs(cardId),
      query = startArgs[0];

  if (!cards.hasCard(query)) {
    cards.removeAllCards();
    pushStartCard(cardId, args);
  }
}

/*
 * Determines if current card is a nonsearch message_list
 * card, which is the default kind of card.
 */
function isCurrentCardMessageList() {
  var cardType = cards.getCurrentCardType();
  return (cardType && cardType === 'message_list');
}

/**
 * Tracks what final card state should be shown. If the
 * app started up hidden for a cronsync, do not actually
 * show the UI until the app becomes visible, so that
 * extra work can be avoided for the hidden cronsync case.
 */
function showFinalCardState(fn) {
  if (startedInBackground && document.hidden) {
    finalCardStateCallback = fn;
  } else {
    fn();
  }
}

/**
 * Shows the message list. Assumes that the correct
 * account and inbox have already been selected.
 */
function showMessageList(args) {
  showFinalCardState(function() {
    resetCards('message_list', args);
  });
}

// Handles visibility changes: if the app becomes visible
// being hidden via a cronsync startup, trigger UI creation.
document.addEventListener('visibilitychange', function onVisibilityChange() {
  if (startedInBackground && finalCardStateCallback && !document.hidden) {
    finalCardStateCallback();
    finalCardStateCallback = null;
  }
}, false);

// The add account UI flow is requested.
evt.on('addAccount', function() {
  cards.removeAllCards();

  // Show the first setup card again.
  pushStartCard('setup_account_info', {
    allowBack: true
  });
});

function resetApp() {
  // Clear any existing local state and reset UI/model state.
  waitForAppMessage = false;
  waitingForCreateAccountPrompt = false;
  activityCallback = null;

  cards.removeAllCards();
  model.init();
}

function activityContinued() {
  if (activityCallback) {
    var activityCb = activityCallback;
    activityCallback = null;
    activityCb();
    return true;
  }
  return false;
}

// An account was deleted. Burn it all to the ground and
// rise like a phoenix. Prefer a UI event vs. a slice
// listen to give flexibility about UI construction:
// an acctsSlice splice change may not warrant removing
// all the cards.
evt.on('accountDeleted', resetApp);
evt.on('resetApp', resetApp);

// A request to show the latest account in the UI.
// Usually triggered after an account has been added.
evt.on('showLatestAccount', function() {
  cards.removeAllCards();

  model.latestOnce('acctsSlice', function(acctsSlice) {
    var account = acctsSlice.items[acctsSlice.items.length - 1];

    model.changeAccount(account, function() {
      pushStartCard('message_list', {
        // If waiting to complete an activity, do so after pushing the
        // message list card.
        onPushed: activityContinued
      });
    });
  });
});

model.on('acctsSlice', function() {
  if (!model.hasAccount()) {
    if (!waitingForCreateAccountPrompt) {
      resetCards('setup_account_info');
    }
  } else {
    model.latestOnce('foldersSlice', function() {
      if (waitForAppMessage) {
        return;
      }

      // If an activity was waiting for an account, trigger it now.
      if (activityContinued()) {
        return;
      }

      showMessageList();
    });
  }
});

// Rate limit rapid fire entries, like an accidental double tap. While the card
// code adjusts for the taps, in the case of configured account, user can end up
// with multiple compose or reader cards in the stack, which is probably
// confusing, and the rapid tapping is likely just an accident, or an incorrect
// user belief that double taps are needed for activation.
// Using one entry time tracker across all gate entries since ideally we do not
// want to handle a second fast action regardless of source. We want the first
// one to have a chance of getting a bit further along. If this becomes an issue
// though, the closure created inside getEntry could track a unique time for
// each gateEntry use.
var lastEntryTime = 0;
function gateEntry(fn) {
  return function() {
    var entryTime = Date.now();
    // Only one entry per second.
    if (entryTime < lastEntryTime + 1000) {
      console.log('email entry gate blocked fast repeated action');
      return;
    }
    lastEntryTime = entryTime;

    return fn.apply(null, slice.call(arguments));
  };
}

appMessages.on('activity', gateEntry(function(type, data, rawActivity) {
  function initComposer() {
    cards.pushCard('compose', 'immediate', {
      activity: rawActivity,
      composerData: {
        onComposer: function(composer, composeCard) {
          var attachmentBlobs = data.attachmentBlobs;
          /* to/cc/bcc/subject/body all have default values that shouldn't
          be clobbered if they are not specified in the URI*/
          if (data.to) {
            composer.to = data.to;
          }
          if (data.subject) {
            composer.subject = data.subject;
          }
          if (data.body) {
            composer.body = { text: data.body };
          }
          if (data.cc) {
            composer.cc = data.cc;
          }
          if (data.bcc) {
            composer.bcc = data.bcc;
          }
          if (attachmentBlobs) {
            var attachmentsToAdd = [];
            for (var iBlob = 0; iBlob < attachmentBlobs.length; iBlob++) {
              attachmentsToAdd.push({
                name: data.attachmentNames[iBlob],
                blob: attachmentBlobs[iBlob]
              });
            }
            composeCard.addAttachmentsSubjectToSizeLimits(attachmentsToAdd);
          }
        }
      }
    });
  }

  function promptEmptyAccount() {
    ConfirmDialog.show(mozL10n.get('setup-empty-account-prompt'),
    function(confirmed) {
      if (!confirmed) {
        rawActivity.postError('cancelled');
      }

      waitingForCreateAccountPrompt = false;

      // No longer need to wait for the activity to complete, it needs
      // normal card flow
      waitForAppMessage = false;

      activityCallback = initComposer;

      // Always just reset to setup account in case the system does
      // not properly close out the email app on a cancelled activity.
      resetCards('setup_account_info');
    });
  }

  // Remove previous cards because the card stack could get
  // weird if inserting a new card that would not normally be
  // at that stack level. Primary concern: going to settings,
  // then trying to add a compose card at that stack level.
  // More importantly, the added card could have a "back"
  // operation that does not mean "back to previous state",
  // but "back in application flowchart". Message list is a
  // good known jump point, so do not needlessly wipe that one
  // out if it is the current one. Message list is a good
  // known jump point, so do not needlessly wipe that one out
  // if it is the current one.
  if (!isCurrentCardMessageList()) {
    cards.removeAllCards();
  }

  if (model.inited) {
    if (model.hasAccount()) {
      initComposer();
    } else {
      waitingForCreateAccountPrompt = true;
      console.log('email waitingForCreateAccountPrompt');
      promptEmptyAccount();
    }
  } else {
    // Be optimistic and start rendering compose as soon as possible
    // In the edge case that email is not configured, then the empty
    // account prompt will be triggered quickly in the next section.
    initComposer();

    waitingForCreateAccountPrompt = true;
    console.log('email waitingForCreateAccountPrompt');
    model.latestOnce('acctsSlice', function activityOnAccount() {
      if (!model.hasAccount()) {
        promptEmptyAccount();
      }
    });
  }
}));

appMessages.on('notification', gateEntry(function(data) {
  data = data || {};

  // Default to message_list in case no type is set, which could happen if email
  // is awoken by a notification that was generated before email switched to use
  // Notifcation.data in 2.2. Previous versions used fragment IDs on iconURLs.
  // By choosing the message list, then at least some UI is shown.
  var type = data.type || 'message_list';
  var folderType = data.folderType || 'inbox';

  model.latestOnce('foldersSlice', function latestFolderSlice() {
    function onCorrectFolder() {
      function onPushed() {
        waitForAppMessage = false;
      }

      // Remove previous cards because the card stack could get
      // weird if inserting a new card that would not normally be
      // at that stack level. Primary concern: going to settings,
      // then trying to add a reader or message list card at that
      // stack level. More importantly, the added card could have
      // a "back" operation that does not mean "back to previous
      // state", but "back in application flowchart". Message
      // list is a good known jump point, so do not needlessly
      // wipe that one out if it is the current one.
      if (!isCurrentCardMessageList()) {
        cards.removeAllCards();
      }

      if (type === 'message_list') {
        showMessageList({
          onPushed: onPushed
        });
      } else if (type === 'message_reader') {
        headerCursor.setCurrentMessageBySuid(data.messageSuid);

        cards.pushCard(type, 'immediate', {
            messageSuid: data.messageSuid,
            onPushed: onPushed
        });
      }
    }

    var acctsSlice = model.acctsSlice,
        accountId = data.accountId;

    // accountId could be undefined if this was a notification that was
    // generated before 2.2. In that case, just default to the current account.
    if (!accountId || model.account.id === accountId) {
      // folderType will often be 'inbox' (in the case of a new message
      // notification) or 'outbox' (in the case of a "failed send"
      // notification).
      return model.selectFirstFolderWithType(folderType, onCorrectFolder);
    } else {
      var newAccount;
      acctsSlice.items.some(function(account) {
        if (account.id === accountId) {
          newAccount = account;
          return true;
        }
      });

      if (newAccount) {
        model.changeAccount(newAccount, function() {
          model.selectFirstFolderWithType(folderType, onCorrectFolder);
        });
      }
    }
  });
}));

/*
 * IMPORTANT: place this event listener after the one for 'notification'. In the
 * case where email is not running and is opened by a notification, these
 * listeners are added after the initial notifications have been received by
 * app_messages, and so the order in which they are registered affect which one
 * is called first for pending listeners. evt.js does not keep an absolute order
 * on all events.
 */
appMessages.on('notificationClosed', gateEntry(function(data) {
  // The system notifies the app of closed messages. This really is not helpful
  // for the email app, but since it wakes up the app, if we do not at least try
  // to show a usable UI, the user could end up with a blank screen. As the app
  // could also have been awakened by sync or just user action and running in
  // the background, we cannot just close the app. So just make sure there is
  // some usable UI.
  if (waitForAppMessage && !cards.getCurrentCardType()) {
    resetApp();
  }
}));

model.init();
});
Beispiel #7
0
  return function syncInit(model, api) {
    var hasBeenVisible = !document.hidden,
        waitingOnCron = {};

    // Let the back end know the app is interactive, not just
    // a quick sync and shutdown case, so that it knows it can
    // do extra work.
    if (hasBeenVisible) {
      api.setInteractive();
    }

    // If the page is ever not hidden, then do not close it later.
    document.addEventListener('visibilitychange',
      function onVisibilityChange() {
        if (!document.hidden) {
          hasBeenVisible = true;
          api.setInteractive();
        }
    }, false);

    // Creates a string key from an array of string IDs. Uses a space
    // separator since that cannot show up in an ID.
    function makeAccountKey(accountIds) {
      return 'id' + accountIds.join(' ');
    }

    var sendNotification;
    if (typeof Notification !== 'function') {
      console.log('email: notifications not available');
      sendNotification = function() {};
    } else {
      sendNotification = function(notificationId, titleL10n, bodyL10n,
                                  iconUrl, data, behavior) {
        console.log('Notification sent for ' + notificationId);

        if (Notification.permission !== 'granted') {
          console.log('email: notification skipped, permission: ' +
                      Notification.permission);
          return;
        }

        data = data || {};

        //TODO: consider setting dir and lang?
        //https://developer.mozilla.org/en-US/docs/Web/API/notification
        var notificationOptions = {
          bodyL10n: bodyL10n,
          icon: iconUrl,
          tag: notificationId,
          data: data,
          mozbehavior: {
            noscreen: true
          }
        };

        if (behavior) {
          Object.keys(behavior).forEach(function(key) {
            notificationOptions.mozbehavior[key] = behavior[key];
          });
        }

        notificationHelper.send(titleL10n, notificationOptions)
          .then(function(notification){
            // If the app is open, but in the background, when the notification
            // comes in, then we do not get notifived via our
            // mozSetMessageHandler that is set elsewhere. Instead need to
            // listen to click event and synthesize an "event" ourselves.
            notification.onclick = function() {
              evt.emit('notification', {
                clicked: true,
                imageURL: iconUrl,
                tag: notificationId,
                data: data
              });
            };
          });
      };
    }

    api.oncronsyncstart = function(accountIds) {
      console.log('email oncronsyncstart: ' + accountIds);
      cronSyncStartTime = Date.now();
      var accountKey = makeAccountKey(accountIds);
      waitingOnCron[accountKey] = true;
    };

    /**
     * Fetches notification data for the notification type, ntype. This method
     * assumes there is only one ntype of notification per account.
     * @param  {String} ntype The notification type, like 'sync'.
     * @return {Promise}      Promise that resolves to a an object whose keys
     * are account IDs and values are notification data.
     */
    function fetchNotificationsData(ntype) {
      if (typeof Notification !== 'function' || !Notification.get) {
        return Promise.resolve({});
      }

      return Notification.get().then(function(notifications) {
        var result = {};
        notifications.forEach(function(notification) {
          var data = notification.data;

          // Want to avoid unexpected data formats. So if not a version match
          // then just close it since it cannot be processed as expected. This
          // means that notifications not generated by this module may be
          // closed. However, ideally only this module generates notifications,
          // for localization of concerns.
          if (!data.v || data.v !== notificationDataVersion) {
            notification.close();
          } else if (data.ntype === ntype) {
            data.notification = notification;
            result[data.accountId] = data;
          }
        });
        return result;
      }, function(err) {
        // Do not care about errors, just log and keep going.
        console.error('email notification.get call failed: ' + err);
        return {};
      });
    }

    /**
     * Helper to just get some environment data for dealing with sync-based
     * notfication data. Exists to reduce the curly brace pyramid of doom and
     * to normalize existing sync notification info.
     * @param {Function} fn function to call once env info is fetched.
     */
    function getSyncEnv(fn) {
      appSelf.latest('self', function(app) {
        model.latestOnce('account', function(currentAccount) {
          fetchNotificationsData('sync').then(
            function(existingNotificationsData) {
              mozL10n.formatValue('senders-separation-sign')
              .then(function(separator) {
                var localized = {
                  separator
                };
                mozL10n.formatValue('notification-no-subject')
                .then(function(noSubject) {
                  localized.noSubject = noSubject;
                  fn(app, currentAccount, existingNotificationsData, localized);
                });
            });
          });
        });
      });
    }

    /**
     * Generates a list of unique top names sorted by most recent sender first,
     * and limited to a max number. The max number is just to limit amount of
     * work and likely display limits.
     * @param  {Array} latestInfos  array of result.latestMessageInfos. Modifies
     * result.latestMessageInfos via a sort.
     * @param  {Array} oldFromNames old from names from a previous notification.
     * @return {Array} a maxFromList array of most recent senders.
     */
    function topUniqueFromNames(latestInfos, oldFromNames) {
      var names = [],
          maxCount = 3;

      // Get the new from senders from the result. First,
      // need to sort by most recent.
      // Note that sort modifies result.latestMessageInfos
      latestInfos.sort(function(a, b) {
       return b.date - a.date;
      });

      // Only need three unique names, and just the name, not
      // the full info object.
      latestInfos.some(function(info) {
        if (names.length > maxCount) {
          return true;
        }

        if (names.indexOf(info.from) === -1) {
          names.push(info.from);
        }
      });

      // Now add in old names to fill out a list of
      // max names.
      oldFromNames.some(function(name) {
        if (names.length > maxCount) {
          return true;
        }
        if (names.indexOf(name) === -1) {
          names.push(name);
        }
      });

      return names;
    }

    /*
    accountsResults is an object with the following structure:
      accountIds: array of string account IDs.
      updates: array of objects includes properties:
        id: accountId,
        name: account name,
        count: number of new messages total
        latestMessageInfos: array of latest message info objects,
        with properties:
          - from
          - subject
          - accountId
          - messageSuid
     */
    api.oncronsyncstop = function(accountsResults) {
      console.log('email oncronsyncstop: ' + accountsResults.accountIds);

      function finishSync() {
        evt.emit('cronSyncStop', accountsResults.accountIds);

        // Mark this accountId set as no longer waiting.
        var accountKey = makeAccountKey(accountsResults.accountIds);
        waitingOnCron[accountKey] = false;
        var stillWaiting = Object.keys(waitingOnCron).some(function(key) {
          return !!waitingOnCron[key];
        });

        if (!hasBeenVisible && !stillWaiting) {
          console.log('sync completed in ' +
                     ((Date.now() - cronSyncStartTime) / 1000) +
                     ' seconds, closing mail app');
          window.close();
        }
      }

      // If no sync updates, wrap it up.
      if (!accountsResults.updates) {
        finishSync();
        return;
      }

      // There are sync updates, get environment and figure out how to notify
      // the user of the updates.
      getSyncEnv(function(
                 app, currentAccount, existingNotificationsData, localized) {
        var iconUrl = notificationHelper.getIconURI(app);

        accountsResults.updates.forEach(function(result) {
          // If the current account is being shown, then just send an update
          // to the model to indicate new messages, as the notification will
          // happen within the app for that case. The 'inboxShown' pathway
          // will be sure to close any existing notification for the current
          // account.
          if (currentAccount.id === result.id && !document.hidden) {
            model.notifyInboxMessages(result);
            return;
          }

          // If this account does not want notifications of new messages
          // or if no Notification object, stop doing work.
          if (!model.getAccount(result.id).notifyOnNew ||
              typeof Notification !== 'function') {
            return;
          }

          var dataObject, subjectL10n, bodyL10n, behavior,
              count = result.count,
              oldFromNames = [];

          // Adjust counts/fromNames based on previous notification.
          var existingData = existingNotificationsData[result.id];
          if (existingData) {
            if (existingData.count) {
              count += parseInt(existingData.count, 10);
            }
            if (existingData.fromNames) {
              oldFromNames = existingData.fromNames;
            }
          }

          if (count > 1) {
            // Multiple messages were synced.
            // topUniqueFromNames modifies result.latestMessageInfos
            var newFromNames = topUniqueFromNames(result.latestMessageInfos,
                                                  oldFromNames);
            dataObject = {
              v: notificationDataVersion,
              ntype: 'sync',
              type: 'message_list',
              accountId: result.id,
              count: count,
              fromNames: newFromNames
            };

            // If already have a notification, then do not bother with sound or
            // vibration for this update. Longer term, the notification standard
            // will have a "silent" option, but using a non-existent URL as
            // suggested in bug 1042361 in the meantime.
            if (existingData && existingData.count) {
              behavior = {
                soundFile: 'does-not-exist-to-simulate-silent',
                // Cannot use 0 since system/js/notifications.js explicitly
                // ignores [0] values. [1] is good enough for this purpose.
                vibrationPattern: [1]
              };
            }

            if (model.getAccountCount() === 1) {
              subjectL10n = {
                id: 'new-emails-notify-one-account',
                args: { n: count }
              };
            } else {
              subjectL10n = {
                id: 'new-emails-notify-multiple-accounts',
                args: {
                  n: count,
                  accountName: result.address
                }
              };
            }

            bodyL10n = { raw: newFromNames.join(localized.separator) };

          } else {
            // Only one message to notify about.
            var info = result.latestMessageInfos[0];
            dataObject = {
              v: notificationDataVersion,
              ntype: 'sync',
              type: 'message_reader',
              accountId: info.accountId,
              messageSuid: info.messageSuid,
              count: 1,
              fromNames: [info.from]
            };

            var rawSubject = info.subject || localized.noSubject;

            if (model.getAccountCount() === 1) {
              subjectL10n = { raw: rawSubject };
              bodyL10n = { raw: info.from };
            } else {
              subjectL10n = {
                id: 'new-emails-notify-multiple-accounts',
                args: {
                  n: count,
                  accountName: result.address
                }
              };
              bodyL10n = {
                id: 'new-emails-notify-multiple-accounts-body',
                args: {
                  from: info.from,
                  subject: rawSubject
                }
              };
            }
          }

          sendNotification(
            result.id,
            subjectL10n,
            bodyL10n,
            iconUrl,
            dataObject,
            behavior
          );
        });

        finishSync();
      });
    };

    // Background Send Notifications

    var BACKGROUND_SEND_NOTIFICATION_ID = 'backgroundSendFailed';
    var sentAudio = null; // Lazy-loaded when first needed

    /**
     * The API passes through background send notifications with the
     * following data (see the "sendOutboxMessages" job and/or
     * `GELAM/js/jobs/outbox.js`):
     *
     * @param {int} accountId
     * @param {string} suid
     *   SUID of the message
     * @param {string} state
     *   'pending', 'syncing', 'success', or 'error'
     * @param {string} err
     *   (if applicable, otherwise null)
     * @param {array} badAddresses
     *   (if applicable)
     * @param {int} sendFailures
     *   Count of the number of times the message failed to send.
     * @param {Boolean} emitNotifications
     *   True if this message is being sent as a direct result of
     *   the user sending a message from the compose window. False
     *   otherwise, as in when the user "refreshes" the outbox.
     * @param {Boolean} willSendMore
     *   True if we will send a subsequent message from the outbox
     *   immediately after sending this message.
     *
     * Additionally, this function appends the following to that
     * structured data:
     *
     * @param {string} localizedDescription Notification text.
     *
     * If the application is in the foreground, we notify the user on
     * both success and failure. If the application is in the
     * background, we only post a system notifiaction on failure.
     */
    api.onbackgroundsendstatus = function(data) {
      console.log('outbox: Message', data.suid, 'status =', JSON.stringify({
        state: data.state,
        err: data.err,
        sendFailures: data.sendFailures,
        emitNotifications: data.emitNotifications
      }));

      // Grab an appropriate localized string here. This description
      // may be displayed in a number of different places, so it's
      // cleaner to do the localization here.

      var descId;
      switch (data.state) {
      case 'pending': descId = 'background-send-pending'; break;
      case 'sending': descId = 'background-send-sending'; break;
      case 'success': descId = 'background-send-success'; break;
      case 'error':
        if ((data.badAddresses && data.badAddresses.length) ||
            data.err === 'bad-recipient') {
          descId = 'background-send-error-recipients';
        } else {
          descId = 'background-send-error';
        }
        break;
      case 'syncDone':
        // We will not display any notification for a 'syncDone'
        // message, except to stop refresh icons from spinning. No
        // need to attempt to populate a description.
        break;
      default:
        console.error('No state description for background send state "' +
                      data.state + '"');
        return;
      }

      // If the message sent successfuly, and we're sending this as a
      // side-effect of the user hitting "send" on the compose screen,
      // (i.e. emitNotifications is true), we may need to play a sound.
      if (data.state === 'success') {
        // Grab an up-to-date reading of the "play sound on send"
        // preference to decide if we're going to play a sound or not.
        model.latestOnce('acctsSlice', function() {
          var account = model.getAccount(data.accountId);
          if (!account) {
            console.error('Invalid account ID', data.accountId,
                          'for a background send notification.');
            return;
          }

          // If email is in the background, we should still be able to
          // play audio due to having the 'audio-channel-notification'
          // permission (unless higher priority audio is playing).

          // TODO: As of June 2014, this behavior is still in limbo;
          // see the following links for relevant discussion. We may
          // need to follow up to ensure we get the behavior we want
          // (which is to play a sound when possible, even if we're in
          // the background).
          //   Thread on dev-gaia: http://goo.gl/l6REZy
          //   AUDIO_COMPETING bugs: https://bugzil.la/911238
          if (account.playSoundOnSend) {
            if (!sentAudio) {
              sentAudio = new Audio('/sounds/firefox_sent.opus');
              sentAudio.mozAudioChannelType = 'notification';
            }
            sentAudio.play();
          }
        }.bind(this));
      }

      // If we are in the foreground, notify through the model, which
      // will display an in-app toast notification when appropriate.
      if (!document.hidden) {
        mozL10n.formatValue(descId).then(function(localizedDescription) {
          data.localizedDescription = localizedDescription;
          model.notifyBackgroundSendStatus(data);
        });
      }
      // Otherwise, notify with a system notification in the case of
      // an error. By design, we don't use system-level notifications
      // to notify the user on success, lest they get inundated with
      // notifications.
      else if (data.state === 'error' && data.emitNotifications) {
        appSelf.latest('self', function(app) {
          var iconUrl = notificationHelper.getIconURI(app);
          var dataObject = {
            v: notificationDataVersion,
            ntype: 'outbox',
            type: 'message_reader',
            folderType: 'outbox',
            accountId: data.accountId,
            messageSuid: data.suid
          };

          sendNotification(
            BACKGROUND_SEND_NOTIFICATION_ID,
            'background-send-error-title',
            descId,
            iconUrl,
            dataObject
          );
        });
      }
    };

    // When inbox is viewed, be sure to clear out any possible notification
    // for that account.
    evt.on('inboxShown', function(accountId) {
      fetchNotificationsData('sync').then(function(notificationsData) {
        if (notificationsData.hasOwnProperty(accountId)) {
          notificationsData[accountId].notification.close();
        }
      });
    });
  };
Beispiel #8
0
M.on = function(evt, handler){
  _evt.on(evt, handler);
};