Source: jsxc.lib.xmpp.js

/**
 * Handle XMPP stuff.
 *
 * @namespace jsxc.xmpp
 */
jsxc.xmpp = {
   conn: null, // connection

   /**
    * Create new connection or attach to old
    *
    * @name login
    * @memberOf jsxc.xmpp
    * @private
    */
   /**
    * Create new connection with given parameters.
    *
    * @name login^2
    * @param {string} jid
    * @param {string} password
    * @memberOf jsxc.xmpp
    * @private
    */
   /**
    * Attach connection with given parameters.
    *
    * @name login^3
    * @param {string} jid
    * @param {string} sid
    * @param {string} rid
    * @memberOf jsxc.xmpp
    * @private
    */
   login: function() {

      if (jsxc.xmpp.conn && jsxc.xmpp.conn.authenticated) {
         jsxc.debug('Connection already authenticated.');
         return;
      }

      var jid = null,
         password = null,
         sid = null,
         rid = null;

      switch (arguments.length) {
         case 2:
            jid = arguments[0];
            password = arguments[1];
            break;
         case 3:
            jid = arguments[0];
            sid = arguments[1];
            rid = arguments[2];
            break;
         default:
            sid = jsxc.storage.getItem('sid');
            rid = jsxc.storage.getItem('rid');

            if (sid !== null && rid !== null) {
               jid = jsxc.storage.getItem('jid');
            } else {
               sid = jsxc.options.xmpp.sid || null;
               rid = jsxc.options.xmpp.rid || null;
               jid = jsxc.options.xmpp.jid;
            }
      }

      if (!jid) {
         jsxc.warn('Jid required for login');

         return;
      }

      if (!jsxc.bid) {
         jsxc.bid = jsxc.jidToBid(jid);
      }

      var url = jsxc.options.get('xmpp').url;

      if (!url) {
         jsxc.warn('xmpp.url required for login');

         return;
      }

      if (!(jsxc.xmpp.conn && jsxc.xmpp.conn.connected)) {
         // Register eventlistener
         $(document).on('connected.jsxc', jsxc.xmpp.connected);
         $(document).on('attached.jsxc', jsxc.xmpp.attached);
         $(document).on('disconnected.jsxc', jsxc.xmpp.disconnected);
         $(document).on('connfail.jsxc', jsxc.xmpp.onConnfail);
         $(document).on('authfail.jsxc', jsxc.xmpp.onAuthFail);

         Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts');
         Strophe.addNamespace('VERSION', 'jabber:iq:version');
      }

      // Create new connection (no login)
      jsxc.xmpp.conn = new Strophe.Connection(url);

      if (jsxc.storage.getItem('debug') === true) {
         jsxc.xmpp.conn.xmlInput = function(data) {
            console.log('<', data);
         };
         jsxc.xmpp.conn.xmlOutput = function(data) {
            console.log('>', data);
         };
      }

      jsxc.xmpp.conn.nextValidRid = jsxc.xmpp.onRidChange;

      var callback = function(status, condition) {

         jsxc.debug(Object.getOwnPropertyNames(Strophe.Status)[status] + ': ' + condition);

         switch (status) {
            case Strophe.Status.CONNECTING:
               $(document).trigger('connecting.jsxc');
               break;
            case Strophe.Status.CONNECTED:
               jsxc.bid = jsxc.jidToBid(jsxc.xmpp.conn.jid.toLowerCase());
               $(document).trigger('connected.jsxc');
               break;
            case Strophe.Status.ATTACHED:
               $(document).trigger('attached.jsxc');
               break;
            case Strophe.Status.DISCONNECTED:
               $(document).trigger('disconnected.jsxc');
               break;
            case Strophe.Status.CONNFAIL:
               $(document).trigger('connfail.jsxc');
               break;
            case Strophe.Status.AUTHFAIL:
               $(document).trigger('authfail.jsxc');
               break;
         }
      };

      if (jsxc.xmpp.conn.caps) {
         jsxc.xmpp.conn.caps.node = 'http://jsxc.org/';
      }

      jsxc.changeState(jsxc.CONST.STATE.ESTABLISHING);

      if (sid && rid) {
         jsxc.debug('Try to attach');
         jsxc.debug('SID: ' + sid);

         jsxc.xmpp.conn.attach(jid, sid, rid, callback);
      } else {
         jsxc.debug('New connection');

         if (jsxc.xmpp.conn.caps) {
            // Add system handler, because user handler isn't called before
            // we are authenticated
            // @REVIEW this could maybe retrieved from jsxc.xmpp.conn.features
            jsxc.xmpp.conn._addSysHandler(function(stanza) {
               var from = jsxc.xmpp.conn.domain,
                  c = stanza.querySelector('c'),
                  ver = c.getAttribute('ver'),
                  node = c.getAttribute('node');

               var _jidNodeIndex = JSON.parse(localStorage.getItem('strophe.caps._jidNodeIndex')) || {};

               jsxc.xmpp.conn.caps._jidVerIndex[from] = ver;
               _jidNodeIndex[from] = node;

               localStorage.setItem('strophe.caps._jidVerIndex', JSON.stringify(jsxc.xmpp.conn.caps._jidVerIndex));
               localStorage.setItem('strophe.caps._jidNodeIndex', JSON.stringify(_jidNodeIndex));
            }, Strophe.NS.CAPS);
         }

         jsxc.xmpp.conn.connect(jid, password || jsxc.options.xmpp.password, callback);
      }
   },

   /**
    * Logs user out of his xmpp session and does some clean up.
    *
    * @param {boolean} complete If set to false, roster will not be removed
    * @returns {Boolean}
    */
   logout: function(complete) {

      jsxc.triggeredFromElement = (typeof complete === 'boolean') ? complete : true;

      if (!jsxc.master) {
         // instruct master
         jsxc.storage.removeItem('sid');

         // jsxc.xmpp.disconnected is called if master deletes alive after logout
         return true;
      }

      // REVIEW: this should maybe moved to xmpp.disconnected
      // clean up
      jsxc.storage.removeUserItem('windowlist');
      jsxc.storage.removeUserItem('unreadMsg');

      if (jsxc.gui.favicon) {
         jsxc.gui.favicon.badge(0);
      }

      // Hide dropdown menu
      $('body').click();

      if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) {
         return true;
      }

      // restore all otr objects
      $.each(jsxc.storage.getUserItem('otrlist') || {}, function(i, val) {
         jsxc.otr.create(val);
      });

      var numOtr = Object.keys(jsxc.otr.objects || {}).length + 1;
      var disReady = function() {
         if (--numOtr <= 0) {
            jsxc.xmpp.conn.flush();

            setTimeout(function() {
               jsxc.xmpp.conn.disconnect();
            }, 600);
         }
      };

      // end all private conversations
      $.each(jsxc.otr.objects || {}, function(key, obj) {
         if (obj.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) {
            obj.endOtr.call(obj, function() {
               obj.init.call(obj);
               jsxc.otr.backup(key);

               disReady();
            });
         } else {
            disReady();
         }
      });

      disReady();

      // Trigger real logout in jsxc.xmpp.disconnected()
      return false;
   },

   /**
    * Triggered if connection is established
    *
    * @private
    */
   connected: function() {

      jsxc.xmpp.conn.pause();

      jsxc.xmpp.initNewConnection();

      jsxc.xmpp.saveSessionParameter();

      var rosterVerSupport = $(jsxc.xmpp.conn.features).find('[xmlns="urn:xmpp:features:rosterver"]').length > 0;
      jsxc.storage.setUserItem('rosterVerSupport', rosterVerSupport);

      if (jsxc.options.loginForm.triggered) {
         switch (jsxc.options.loginForm.onConnected || 'submit') {
            case 'submit':
               jsxc.submitLoginForm();
               return;
            case false:
               return;
         }
      }

      // start chat

      jsxc.gui.dialog.close();

      jsxc.xmpp.conn.resume();
      jsxc.onMaster();

      jsxc.changeState(jsxc.CONST.STATE.READY);
      $(document).trigger('attached.jsxc');
   },

   /**
    * Triggered if connection is attached
    *
    * @private
    */
   attached: function() {

      $('#jsxc_roster').removeClass('jsxc_noConnection');

      Strophe.addNamespace('VERSION', 'jabber:iq:version');

      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onRosterChanged, 'jabber:iq:roster', 'iq', 'set');
      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onChatMessage, null, 'message', 'chat');
      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onErrorMessage, null, 'message', 'error');
      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onHeadlineMessage, null, 'message', 'headline');
      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onReceived, null, 'message');
      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onPresence, null, 'presence');
      jsxc.xmpp.conn.addHandler(jsxc.xmpp.onVersionRequest, Strophe.NS.VERSION, 'iq', 'get');

      jsxc.gui.init();

      var caps = jsxc.xmpp.conn.caps;
      var domain = jsxc.xmpp.conn.domain;

      if (caps) {
         var conditionalEnable = function() {};

         if (jsxc.options.get('carbons').enable) {
            conditionalEnable = function() {
               if (jsxc.xmpp.conn.caps.hasFeatureByJid(domain, jsxc.CONST.NS.CARBONS)) {
                  jsxc.xmpp.carbons.enable();
               }
            };

            $(document).on('caps.strophe', function onCaps(ev, from) {

               if (from !== domain) {
                  return;
               }

               conditionalEnable();

               $(document).off('caps.strophe', onCaps);
            });
         }

         if (typeof caps._knownCapabilities[caps._jidVerIndex[domain]] === 'undefined') {
            var _jidNodeIndex = JSON.parse(localStorage.getItem('strophe.caps._jidNodeIndex')) || {};

            jsxc.debug('Request server capabilities');

            caps._requestCapabilities(jsxc.xmpp.conn.domain, _jidNodeIndex[domain], caps._jidVerIndex[domain]);
         } else {
            // We know server caps
            conditionalEnable();
         }
      }

      var rosterLoaded = jsxc.storage.getUserItem('rosterLoaded');

      // Only load roaster if necessary
      if (rosterLoaded !== jsxc.xmpp.conn._proto.sid) {
         // in order to not overide existing presence information, we send
         // pres first after roster is ready
         $(document).one('cloaded.roster.jsxc', jsxc.xmpp.sendPres);

         $('#jsxc_roster > p:first').remove();

         var queryAttr = {
            xmlns: 'jabber:iq:roster'
         };

         if (jsxc.storage.getUserItem('rosterVerSupport')) {
            // @TODO check if we really cached the roster
            queryAttr.ver = jsxc.storage.getUserItem('rosterVer') || '';
         }

         var iq = $iq({
            type: 'get'
         }).c('query', queryAttr);

         jsxc.xmpp.conn.sendIQ(iq, jsxc.xmpp.onRoster);
      } else {
         jsxc.xmpp.sendPres();

         if (!jsxc.restoreCompleted) {
            jsxc.gui.restore();
         }
      }

      jsxc.xmpp.saveSessionParameter();

      jsxc.masterActions();

      jsxc.changeState(jsxc.CONST.STATE.READY);
   },

   saveSessionParameter: function() {

      var nomJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid).toLowerCase() + '/' + Strophe.getResourceFromJid(jsxc.xmpp.conn.jid);

      // Save sid and jid
      jsxc.storage.setItem('sid', jsxc.xmpp.conn._proto.sid);
      jsxc.storage.setItem('jid', nomJid);
   },

   initNewConnection: function() {
      jsxc.storage.removeUserItem('windowlist');
      jsxc.storage.removeUserItem('own');
      jsxc.storage.removeUserItem('avatar', 'own');
      jsxc.storage.removeUserItem('otrlist');
      jsxc.storage.removeUserItem('unreadMsg');
      jsxc.storage.removeUserItem('features');

      // reset user options
      jsxc.storage.removeUserElement('options', 'RTCPeerConfig');
   },

   /**
    * Sends presence stanza to server.
    */
   sendPres: function() {
      // disco stuff
      if (jsxc.xmpp.conn.disco) {
         jsxc.xmpp.conn.disco.addIdentity('client', 'web', 'JSXC');
         jsxc.xmpp.conn.disco.addFeature(Strophe.NS.DISCO_INFO);
         jsxc.xmpp.conn.disco.addFeature(Strophe.NS.RECEIPTS);
         jsxc.xmpp.conn.disco.addFeature(Strophe.NS.VERSION);
      }

      // create presence stanza
      var pres = $pres();

      if (jsxc.xmpp.conn.caps) {
         // attach caps
         pres.c('c', jsxc.xmpp.conn.caps.generateCapsAttrs()).up();
      }

      var presState = jsxc.storage.getUserItem('presence') || 'online';
      if (presState !== 'online') {
         pres.c('show').t(presState).up();
      }

      var priority = jsxc.options.get('priority');
      if (priority && typeof priority[presState] !== 'undefined' && parseInt(priority[presState]) !== 0) {
         pres.c('priority').t(priority[presState]).up();
      }

      jsxc.debug('Send presence', pres.toString());
      jsxc.xmpp.conn.send(pres);

      if (!jsxc.storage.getUserItem('features')) {
         jsxc.xmpp.conn.flush();

         var barJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid);

         jsxc.xmpp.conn.disco.info(barJid, undefined, function(stanza) {
            var features = $(stanza).find('feature').map(function() {
               return $(this).attr('var');
            });

            jsxc.storage.setUserItem('features', features.toArray());
            $(document).trigger('features.jsxc');
         });
      } else {
         $(document).trigger('features.jsxc');
      }
   },

   /**
    * Triggered if lost connection
    *
    * @private
    */
   disconnected: function() {
      jsxc.debug('disconnected');

      jsxc.storage.removeItem('jid');
      jsxc.storage.removeItem('sid');
      jsxc.storage.removeItem('rid');
      jsxc.storage.removeItem('hidden');
      jsxc.storage.removeUserItem('avatar', 'own');
      jsxc.storage.removeUserItem('otrlist');
      jsxc.storage.removeUserItem('features');

      $(document).off('connected.jsxc', jsxc.xmpp.connected);
      $(document).off('attached.jsxc', jsxc.xmpp.attached);
      $(document).off('disconnected.jsxc', jsxc.xmpp.disconnected);
      $(document).off('connfail.jsxc', jsxc.xmpp.onConnfail);
      $(document).off('authfail.jsxc', jsxc.xmpp.onAuthFail);

      jsxc.xmpp.conn = null;

      $('#jsxc_windowList').remove();

      if (jsxc.triggeredFromElement) {
         $(document).trigger('toggle.roster.jsxc', ['hidden', 0]);
         jsxc.gui.roster.ready = false;
         $('#jsxc_roster').remove();

         // REVIEW: logoutElement without href attribute?
         if (jsxc.triggeredFromLogout) {
            window.location = jsxc.options.logoutElement.attr('href');
         }
      } else {
         jsxc.gui.roster.noConnection();
      }

      window.clearInterval(jsxc.keepaliveInterval);
      jsxc.role_allocation = false;
      jsxc.master = false;
      jsxc.storage.removeItem('alive');

      jsxc.changeState(jsxc.CONST.STATE.SUSPEND);
   },

   /**
    * Triggered on connection fault
    *
    * @param {String} condition information why we lost the connection
    * @private
    */
   onConnfail: function(ev, condition) {
      jsxc.debug('XMPP connection failed: ' + condition);

      if (jsxc.options.loginForm.triggered) {
         jsxc.submitLoginForm();
      }
   },

   /**
    * Triggered on auth fail.
    *
    * @private
    */
   onAuthFail: function() {

      if (jsxc.options.loginForm.triggered) {
         switch (jsxc.options.loginForm.onAuthFail || 'ask') {
            case 'ask':
               jsxc.gui.showAuthFail();
               break;
            case 'submit':
               jsxc.submitLoginForm();
               break;
            case 'quiet':
            case false:
               return;
         }
      }
   },

   /**
    * Triggered on initial roster load
    *
    * @param {dom} iq
    * @private
    */
   onRoster: function(iq) {
      jsxc.debug('Load roster', iq);

      jsxc.storage.setUserItem('rosterLoaded', jsxc.xmpp.conn._proto.sid);

      if ($(iq).find('query').length === 0) {
         jsxc.debug('Use cached roster');

         jsxc.restoreRoster();
         return;
      }

      var buddies = [];

      $(iq).find('item').each(function() {
         var jid = $(this).attr('jid');
         var name = $(this).attr('name') || jid;
         var bid = jsxc.jidToBid(jid);
         var sub = $(this).attr('subscription');

         buddies.push(bid);

         jsxc.storage.removeUserItem('res', bid);

         jsxc.storage.saveBuddy(bid, {
            jid: jid,
            name: name,
            status: 0,
            sub: sub,
            res: [],
            rnd: Math.random() // force storage event
         });

         jsxc.gui.roster.add(bid);
      });

      if (buddies.length === 0) {
         jsxc.gui.roster.empty();
      }

      jsxc.storage.setUserItem('buddylist', buddies);

      if ($(iq).find('query').attr('ver')) {
         jsxc.storage.setUserItem('rosterVer', $(iq).find('query').attr('ver'));
      }

      // load bookmarks
      jsxc.xmpp.bookmarks.load();

      jsxc.gui.roster.loaded = true;
      jsxc.debug('Roster loaded');
      $(document).trigger('cloaded.roster.jsxc');
      jsxc.changeUIState(jsxc.CONST.UISTATE.READY);
   },

   /**
    * Triggerd on roster changes
    *
    * @param {dom} iq
    * @returns {Boolean} True to preserve handler
    * @private
    */
   onRosterChanged: function(iq) {

      var iqSender = $(iq).attr('from');
      var ownBareJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid);

      if (iqSender && iqSender !== ownBareJid) {
         return true;
      }

      jsxc.debug('onRosterChanged', iq);

      // @REVIEW there should be only one item, according to RFC6121
      // https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
      $(iq).find('item').each(function() {
         var jid = $(this).attr('jid');
         var name = $(this).attr('name') || jid;
         var bid = jsxc.jidToBid(jid);
         var sub = $(this).attr('subscription');
         // var ask = $(this).attr('ask');

         if (sub === 'remove') {
            jsxc.gui.roster.purge(bid);
         } else {
            var bl = jsxc.storage.getUserItem('buddylist');

            if (bl.indexOf(bid) < 0) {
               bl.push(bid); // (INFO) push returns the new length
               jsxc.storage.setUserItem('buddylist', bl);
            }

            var temp = jsxc.storage.saveBuddy(bid, {
               jid: jid,
               name: name,
               sub: sub
            });

            if (temp === 'updated') {

               jsxc.gui.update(bid);
               jsxc.gui.roster.reorder(bid);
            } else {
               jsxc.gui.roster.add(bid);
            }
         }

         // Remove pending friendship request from notice list
         if (sub === 'from' || sub === 'both') {
            var notices = jsxc.storage.getUserItem('notices');
            var noticeKey = null,
               notice;

            for (noticeKey in notices) {
               notice = notices[noticeKey];

               if (notice.fnName === 'gui.showApproveDialog' && notice.fnParams[0] === jid) {
                  jsxc.debug('Remove notice with key ' + noticeKey);

                  jsxc.notice.remove(noticeKey);
               }
            }
         }
      });

      if ($(iq).find('query').attr('ver')) {
         jsxc.storage.setUserItem('rosterVer', $(iq).find('query').attr('ver'));
      }

      if (!jsxc.storage.getUserItem('buddylist') || jsxc.storage.getUserItem('buddylist').length === 0) {
         jsxc.gui.roster.empty();
      } else {
         $('#jsxc_roster > p:first').remove();
      }

      // preserve handler
      return true;
   },

   /**
    * Triggered on incoming presence stanzas
    *
    * @param {dom} presence
    * @private
    */
   onPresence: function(presence) {
      /*
       * <presence xmlns='jabber:client' type='unavailable' from='' to=''/>
       *
       * <presence xmlns='jabber:client' from='' to=''> <priority>5</priority>
       * <c xmlns='http://jabber.org/protocol/caps'
       * node='http://psi-im.org/caps' ver='caps-b75d8d2b25' ext='ca cs
       * ep-notify-2 html'/> </presence>
       *
       * <presence xmlns='jabber:client' from='' to=''> <show>chat</show>
       * <status></status> <priority>5</priority> <c
       * xmlns='http://jabber.org/protocol/caps' node='http://psi-im.org/caps'
       * ver='caps-b75d8d2b25' ext='ca cs ep-notify-2 html'/> </presence>
       */
      jsxc.debug('onPresence', presence);

      var ptype = $(presence).attr('type');
      var from = $(presence).attr('from');
      var jid = Strophe.getBareJidFromJid(from).toLowerCase();
      var r = Strophe.getResourceFromJid(from);
      var bid = jsxc.jidToBid(jid);
      var data = jsxc.storage.getUserItem('buddy', bid) || {};
      var res = jsxc.storage.getUserItem('res', bid) || {};
      var status = null;
      var xVCard = $(presence).find('x[xmlns="vcard-temp:x:update"]');

      if (jid === Strophe.getBareJidFromJid(jsxc.storage.getItem("jid"))) {
         return true;
      }

      if (ptype === 'error') {
         $(document).trigger('error.presence.jsxc', [from, presence]);

         var error = $(presence).find('error');

         //@TODO display error message
         jsxc.error('[XMPP] ' + error.attr('code') + ' ' + error.find(">:first-child").prop('tagName'));
         return true;
      }

      // incoming friendship request
      if (ptype === 'subscribe') {
         var bl = jsxc.storage.getUserItem('buddylist');

         if (bl.indexOf(bid) > -1) {
            jsxc.debug('Auto approve contact request, because he is already in our contact list.');

            jsxc.xmpp.resFriendReq(jid, true);
            if (data.sub !== 'to') {
               jsxc.xmpp.addBuddy(jid, data.name);
            }

            return true;
         }

         jsxc.storage.setUserItem('friendReq', {
            jid: jid,
            approve: -1
         });
         jsxc.notice.add({
            msg: $.t('Friendship_request'),
            description: $.t('from') + ' ' + jid,
            type: 'contact'
         }, 'gui.showApproveDialog', [jid]);

         return true;
      } else if (ptype === 'unavailable' || ptype === 'unsubscribed') {
         status = jsxc.CONST.STATUS.indexOf('offline');
      } else {
         var show = $(presence).find('show').text();
         if (show === '') {
            status = jsxc.CONST.STATUS.indexOf('online');
         } else {
            status = jsxc.CONST.STATUS.indexOf(show);
         }
      }

      if (status === 0) {
         delete res[r];
      } else if (r) {
         res[r] = status;
      }

      var maxVal = [];
      var max = 0,
         prop = null;
      for (prop in res) {
         if (res.hasOwnProperty(prop)) {
            if (max <= res[prop]) {
               if (max !== res[prop]) {
                  maxVal = [];
                  max = res[prop];
               }
               maxVal.push(prop);
            }
         }
      }

      if (data.status === 0 && max > 0) {
         // buddy has come online
         jsxc.notification.notify({
            title: data.name,
            msg: $.t('has_come_online'),
            source: bid
         });
      }

      if (data.type !== 'groupchat') {
         data.status = max;
      }

      data.res = maxVal;
      data.jid = jid;

      // Looking for avatar
      if (xVCard.length > 0 && data.type !== 'groupchat') {
         var photo = xVCard.find('photo');

         if (photo.length > 0 && photo.text() !== data.avatar) {
            jsxc.storage.removeUserItem('avatar', data.avatar);
            data.avatar = photo.text();
         }
      }

      // Reset jid
      if (jsxc.gui.window.get(bid).length > 0) {
         jsxc.gui.window.get(bid).data('jid', jid);
      }

      jsxc.storage.setUserItem('buddy', bid, data);
      jsxc.storage.setUserItem('res', bid, res);

      jsxc.debug('Presence (' + from + '): ' + jsxc.CONST.STATUS[status]);

      jsxc.gui.update(bid);
      jsxc.gui.roster.reorder(bid);

      $(document).trigger('presence.jsxc', [from, status, presence]);

      // preserve handler
      return true;
   },

   /**
    * Triggered on incoming message stanzas
    *
    * @param {dom} presence
    * @returns {Boolean}
    * @private
    */
   onChatMessage: function(stanza) {
      var forwarded = $(stanza).find('forwarded[xmlns="' + jsxc.CONST.NS.FORWARD + '"]');
      var message, carbon;
      var originalSender = $(stanza).attr('from');

      if (forwarded.length > 0) {
         message = forwarded.find('> message');
         forwarded = true;
         carbon = $(stanza).find('> [xmlns="' + jsxc.CONST.NS.CARBONS + '"]');

         if (carbon.length === 0) {
            carbon = false;
         } else if (originalSender !== Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid)) {
            // ignore this carbon copy
            return true;
         }

         jsxc.debug('Incoming forwarded message', message);
      } else {
         message = stanza;
         forwarded = false;
         carbon = false;

         jsxc.debug('Incoming message', message);
      }

      var body = $(message).find('body:first').text();
      var htmlBody = $(message).find('body[xmlns="' + Strophe.NS.XHTML + '"]');

      if (!body || (body.match(/\?OTR/i) && forwarded)) {
         return true;
      }

      var type = $(message).attr('type');
      var from = $(message).attr('from');
      var mid = $(message).attr('id');
      var bid;

      var delay = $(message).find('delay[xmlns="urn:xmpp:delay"]');

      var stamp = (delay.length > 0) ? new Date(delay.attr('stamp')) : new Date();
      stamp = stamp.getTime();

      if (carbon) {
         var direction = (carbon.prop("tagName") === 'sent') ? jsxc.Message.OUT : jsxc.Message.IN;
         bid = jsxc.jidToBid((direction === 'out') ? $(message).attr('to') : from);

         jsxc.gui.window.postMessage({
            bid: bid,
            direction: direction,
            msg: body,
            encrypted: false,
            forwarded: forwarded,
            stamp: stamp
         });

         return true;

      } else if (forwarded) {
         // Someone forwarded a message to us

         body = from + ' ' + $.t('to') + ' ' + $(stanza).attr('to') + '"' + body + '"';

         from = $(stanza).attr('from');
      }

      var jid = Strophe.getBareJidFromJid(from);
      bid = jsxc.jidToBid(jid);
      var data = jsxc.storage.getUserItem('buddy', bid);
      var request = $(message).find("request[xmlns='urn:xmpp:receipts']");

      if (data === null) {
         // jid not in roster

         var chat = jsxc.storage.getUserItem('chat', bid) || [];

         if (chat.length === 0) {
            jsxc.notice.add({
               msg: $.t('Unknown_sender'),
               description: $.t('You_received_a_message_from_an_unknown_sender_') + ' (' + bid + ').'
            }, 'gui.showUnknownSender', [bid]);
         }

         var msg = jsxc.removeHTML(body);
         msg = jsxc.escapeHTML(msg);

         var messageObj = new jsxc.Message({
            bid: bid,
            msg: msg,
            direction: jsxc.Message.IN,
            encrypted: false,
            forwarded: forwarded,
            stamp: stamp
         });
         messageObj.save();

         return true;
      }

      var win = jsxc.gui.window.init(bid);

      // If we now the full jid, we use it
      if (type === 'chat') {
         win.data('jid', from);
         jsxc.storage.updateUserItem('buddy', bid, {
            jid: from
         });
      }

      $(document).trigger('message.jsxc', [from, body]);

      // create related otr object
      if (jsxc.master && !jsxc.otr.objects[bid]) {
         jsxc.otr.create(bid);
      }

      if (!forwarded && mid !== null && request.length && data !== null && (data.sub === 'both' || data.sub === 'from') && type === 'chat') {
         // Send received according to XEP-0184
         jsxc.xmpp.conn.send($msg({
            to: from
         }).c('received', {
            xmlns: 'urn:xmpp:receipts',
            id: mid
         }));
      }

      var attachment;
      if (htmlBody.length === 1) {
         var httpUploadElement = htmlBody.find('a[data-type][data-name][data-size]');

         if (httpUploadElement.length === 1) {
            // deprecated syntax @since 3.2.1
            attachment = {
               type: httpUploadElement.attr('data-type'),
               name: httpUploadElement.attr('data-name'),
               size: httpUploadElement.attr('data-size'),
            };

            if (httpUploadElement.attr('data-thumbnail') && httpUploadElement.attr('data-thumbnail').match(/^\s*data:[a-z]+\/[a-z0-9-+.*]+;base64,[a-z0-9=+/]+$/i)) {
               attachment.thumbnail = httpUploadElement.attr('data-thumbnail');
            }

            if (httpUploadElement.attr('href') && httpUploadElement.attr('href').match(/^https:\/\//)) {
               attachment.data = httpUploadElement.attr('href');
               body = null;
            }

            if (!attachment.type.match(/^[a-z]+\/[a-z0-9-+.*]+$/i) || !attachment.name.match(/^[\s\w.,-]+$/i) || !attachment.size.match(/^\d+$/i)) {
               attachment = undefined;

               jsxc.warn('Invalid file type, name or size.');
            }
         } else if (htmlBody.find('>a').length === 1) {
            var linkElement = htmlBody.find('>a');
            var metaString = '';
            var thumbnail;

            if (linkElement.find('>img').length === 1) {
               var imgElement = linkElement.find('>img');
               var src = imgElement.attr('src') || '';
               var altString = imgElement.attr('alt') || '';
               metaString = altString.replace(/^Preview:/, '');

               if (src.match(/^\s*data:[a-z]+\/[a-z0-9-+.*]+;base64,[a-z0-9=+/]+$/i)) {
                  thumbnail = src;
               }
            } else {
               metaString = linkElement.text();
            }

            var metaMatch = metaString.match(/^([a-z]+\/[a-z0-9-+.*]+)\|(\d+)\|([\s\w.,-]+)/);

            if (metaMatch) {
               attachment = {
                  type: metaMatch[1],
                  size: metaMatch[2],
                  name: metaMatch[3],
               };

               if (thumbnail) {
                  attachment.thumbnail = thumbnail;
               }

               if (linkElement.attr('href') && linkElement.attr('href').match(/^https?:\/\//)) {
                  attachment.data = linkElement.attr('href');
                  body = null;
               }
            } else {
               jsxc.warn('Invalid file type, name or size.');
            }
         }
      }

      if (jsxc.otr.objects.hasOwnProperty(bid) && body) {
         // @TODO check for file upload url after decryption
         jsxc.otr.objects[bid].receiveMsg(body, {
            _uid: mid,
            stamp: stamp,
            forwarded: forwarded,
            attachment: attachment
         });
      } else {
         jsxc.gui.window.postMessage({
            _uid: mid,
            bid: bid,
            direction: jsxc.Message.IN,
            msg: body,
            encrypted: false,
            forwarded: forwarded,
            stamp: stamp,
            attachment: attachment
         });
      }

      // preserve handler
      return true;
   },

   onErrorMessage: function(message) {
      var bid = jsxc.jidToBid($(message).attr('from'));

      if (jsxc.gui.window.get(bid).length === 0 || !$(message).attr('id')) {
         return true;
      }

      if ($(message).find('item-not-found').length > 0) {
         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('message_not_send_item-not-found')
         });
      } else if ($(message).find('forbidden').length > 0) {
         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('message_not_send_forbidden')
         });
      } else if ($(message).find('not-acceptable').length > 0) {
         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('message_not_send_not-acceptable')
         });
      } else if ($(message).find('remote-server-not-found').length > 0) {
         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('message_not_send_remote-server-not-found')
         });
      } else if ($(message).find('service-unavailable').length > 0) {
         if ($(message).find('[xmlns="' + Strophe.NS.CHATSTATES + '"]').length === 0) {
            jsxc.gui.window.postMessage({
               bid: bid,
               direction: jsxc.Message.SYS,
               msg: $.t('message_not_send_resource-unavailable')
            });
         }
      } else {
         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('message_not_send')
         });
      }

      jsxc.debug('error message for ' + bid, $(message).find('error')[0]);

      return true;
   },

   /**
    * Process message stanzas of type headline.
    *
    * @param  {String} stanza Message stanza of type headline
    * @return {Boolean}
    */
   onHeadlineMessage: function(stanza) {
      stanza = $(stanza);

      var from = stanza.attr('from');
      var domain = Strophe.getDomainFromJid(from);

      if (domain !== from) {
         if (!jsxc.storage.getUserItem('buddy', jsxc.jidToBid(from))) {
            return true;
         }
      } else if (domain !== Strophe.getDomainFromJid(jsxc.xmpp.conn.jid)) {
         return true;
      }

      var subject = stanza.find('subject:first').text() || $.t('Notification');
      var body = stanza.find('body:first').text();

      jsxc.notice.add({
         msg: subject,
         description: body,
         type: (domain === from) ? 'announcement' : null
      }, 'gui.showNotification', [subject, body, from]);

      return true;
   },

   /**
    * Respond to version request (XEP-0092).
    */
   onVersionRequest: function(stanza) {
      stanza = $(stanza);

      var from = stanza.attr('from');
      var id = stanza.attr('id');

      var iq = $iq({
            type: 'result',
            to: from,
            id: id
         }).c('query', {
            xmlns: Strophe.NS.VERSION
         }).c('name').t('JSXC').up()
         .c('version').t(jsxc.version);

      jsxc.xmpp.conn.sendIQ(iq);

      return true;
   },

   /**
    * Triggerd if the rid changed
    *
    * @param {integer} rid next valid request id
    * @private
    */
   onRidChange: function(rid) {
      jsxc.storage.setItem('rid', rid);
   },

   /**
    * response to friendship request
    *
    * @param {string} from jid from original friendship req
    * @param {boolean} approve
    */
   resFriendReq: function(from, approve) {
      if (jsxc.master) {
         jsxc.xmpp.conn.send($pres({
            to: from,
            type: (approve) ? 'subscribed' : 'unsubscribed'
         }));

         jsxc.storage.removeUserItem('friendReq');
         jsxc.gui.dialog.close();

      } else {
         jsxc.storage.updateUserItem('friendReq', 'approve', approve);
      }
   },

   /**
    * Add buddy to my friends
    *
    * @param {string} username jid
    * @param {string} alias
    */
   addBuddy: function(username, alias) {
      var bid = jsxc.jidToBid(username);

      if (jsxc.master) {
         // add buddy to roster (trigger onRosterChanged)
         var iq = $iq({
            type: 'set'
         }).c('query', {
            xmlns: 'jabber:iq:roster'
         }).c('item', {
            jid: username,
            name: alias || ''
         });
         jsxc.xmpp.conn.sendIQ(iq);

         // send subscription request to buddy (trigger onRosterChanged)
         jsxc.xmpp.conn.send($pres({
            to: username,
            type: 'subscribe'
         }));

         jsxc.storage.removeUserItem('add', bid);
      } else {
         jsxc.storage.setUserItem('add', bid, {
            username: username,
            alias: alias || null
         });
      }
   },

   /**
    * Remove buddy from my friends
    *
    * @param {type} jid
    */
   removeBuddy: function(jid) {
      var bid = jsxc.jidToBid(jid);

      // Shortcut to remove buddy from roster and cancle all subscriptions
      var iq = $iq({
         type: 'set'
      }).c('query', {
         xmlns: 'jabber:iq:roster'
      }).c('item', {
         jid: Strophe.getBareJidFromJid(jid),
         subscription: 'remove'
      });
      jsxc.xmpp.conn.sendIQ(iq);

      jsxc.gui.roster.purge(bid);
   },

   onReceived: function(stanza) {
      var received = $(stanza).find("received[xmlns='urn:xmpp:receipts']");

      if (received.length) {
         var receivedId = received.attr('id');
         var message = new jsxc.Message(receivedId);

         message.received();
      }

      return true;
   },

   /**
    * Public function to send message.
    *
    * @memberOf jsxc.xmpp
    * @param bid css jid of user
    * @param msg message
    * @param uid unique id
    */
   sendMessage: function(message) {
      var bid = message.bid;
      var msg = message.msg;

      var mucRoomNames = (jsxc.xmpp.conn.muc && jsxc.xmpp.conn.muc.roomNames) ? jsxc.xmpp.conn.muc.roomNames : [];
      var isMucBid = mucRoomNames.indexOf(bid) >= 0;

      if (jsxc.otr.objects.hasOwnProperty(bid) && !isMucBid) {
         jsxc.otr.objects[bid].sendMsg(msg, message);
      } else {
         jsxc.xmpp._sendMessage(jsxc.gui.window.get(bid).data('jid'), msg, message);
      }
   },

   /**
    * Create message stanza and send it.
    *
    * @memberOf jsxc.xmpp
    * @param jid Jabber id
    * @param msg Message
    * @param uid unique id
    * @private
    */
   _sendMessage: function(jid, msg, message) {
      // @TODO put jid into message object
      var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(jid)) || {};
      var isBar = (Strophe.getBareJidFromJid(jid) === jid);
      var type = data.type || 'chat';
      message = message || {};

      var xmlMsg = $msg({
         to: jid,
         type: type,
         id: message._uid
      });

      if (message.type === jsxc.Message.HTML && msg === message.msg && message.htmlMsg) {
         xmlMsg.c('body').t(msg);

         xmlMsg.up().c('html', {
            xmlns: Strophe.NS.XHTML_IM
         }).c('body', {
            xmlns: Strophe.NS.XHTML
         }).h(message.htmlMsg).up();
      } else {
         xmlMsg.c('body').t(msg);
      }

      if (jsxc.xmpp.carbons.enabled && msg.match(/^\?OTR/)) {
         xmlMsg.up().c("private", {
            xmlns: jsxc.CONST.NS.CARBONS
         });
      }

      if (msg.match(/^\?OTR/)) {
         xmlMsg.up().c("no-permanent-store", {
            xmlns: jsxc.CONST.NS.HINTS
         });
      }

      if (type === 'chat' && (isBar || jsxc.xmpp.conn.caps.hasFeatureByJid(jid, Strophe.NS.RECEIPTS))) {
         // Add request according to XEP-0184
         xmlMsg.up().c('request', {
            xmlns: 'urn:xmpp:receipts'
         });
      }

      if (jsxc.xmpp.conn.chatstates && !jsxc.xmpp.chatState.isDisabled()) {
         // send active event (XEP-0085)
         xmlMsg.up().c('active', {
            xmlns: Strophe.NS.CHATSTATES
         });
      }

      jsxc.xmpp.conn.send(xmlMsg);
   },

   /**
    * This function loads a vcard.
    *
    * @memberOf jsxc.xmpp
    * @param bid
    * @param cb
    * @param error_cb
    */
   loadVcard: function(bid, cb, error_cb) {
      if (jsxc.master) {
         jsxc.xmpp.conn.vcard.get(cb, bid, error_cb);
      } else {
         jsxc.storage.setUserItem('vcard', bid, 'request:' + (new Date()).getTime());

         $(document).one('loaded.vcard.jsxc', function(ev, result) {
            if (result && result.state === 'success') {
               cb($(result.data).get(0));
            } else {
               error_cb();
            }
         });
      }
   },

   /**
    * Retrieves capabilities.
    *
    * @memberOf jsxc.xmpp
    * @param jid
    * @returns List of known capabilities
    */
   getCapabilitiesByJid: function(jid) {
      if (jsxc.xmpp.conn) {
         return jsxc.xmpp.conn.caps.getCapabilitiesByJid(jid);
      }

      var jidVerIndex = JSON.parse(localStorage.getItem('strophe.caps._jidVerIndex')) || {};
      var knownCapabilities = JSON.parse(localStorage.getItem('strophe.caps._knownCapabilities')) || {};

      if (jidVerIndex[jid]) {
         return knownCapabilities[jidVerIndex[jid]];
      }

      return null;
   },

   /**
    * Test if jid has given features
    *
    * @param  {string}   jid     Jabber id
    * @param  {string[]} feature Single feature or list of features
    * @param  {Function} cb      Called with the result as first param.
    * @return {boolean}          True, if jid has all given features. Null, if we do not know it currently.
    */
   hasFeatureByJid: function(jid, feature, cb) {
      var conn = jsxc.xmpp.conn;
      cb = cb || function() {};

      if (!feature) {
         return false;
      }

      if (!$.isArray(feature)) {
         feature = $.makeArray(feature);
      }

      var check = function(knownCapabilities) {
         if (!knownCapabilities) {
            return null;
         }
         var i;
         for (i = 0; i < feature.length; i++) {
            if (knownCapabilities['features'].indexOf(feature[i]) < 0) {
               return false;
            }
         }
         return true;
      };

      if (conn.caps._jidVerIndex[jid] && conn.caps._knownCapabilities[conn.caps._jidVerIndex[jid]]) {
         var hasFeature = check(conn.caps._knownCapabilities[conn.caps._jidVerIndex[jid]]);
         cb(hasFeature);

         return hasFeature;
      }

      $(document).on('strophe.caps', function(ev, j, capabilities) {
         if (j === jid) {
            cb(check(capabilities));

            $(document).off(ev);
         }
      });

      return null;
   }
};

/**
 * Handle carbons (XEP-0280);
 *
 * @namespace jsxc.xmpp.carbons
 */
jsxc.xmpp.carbons = {
   enabled: false,

   /**
    * Enable carbons.
    *
    * @memberOf jsxc.xmpp.carbons
    * @param cb callback
    */
   enable: function(cb) {
      var iq = $iq({
         type: 'set'
      }).c('enable', {
         xmlns: jsxc.CONST.NS.CARBONS
      });

      jsxc.xmpp.conn.sendIQ(iq, function() {
         jsxc.xmpp.carbons.enabled = true;

         jsxc.debug('Carbons enabled');

         if (cb) {
            cb.call(this);
         }
      }, function(stanza) {
         jsxc.warn('Could not enable carbons', stanza);
      });
   },

   /**
    * Disable carbons.
    *
    * @memberOf jsxc.xmpp.carbons
    * @param cb callback
    */
   disable: function(cb) {
      var iq = $iq({
         type: 'set'
      }).c('disable', {
         xmlns: jsxc.CONST.NS.CARBONS
      });

      jsxc.xmpp.conn.sendIQ(iq, function() {
         jsxc.xmpp.carbons.enabled = false;

         jsxc.debug('Carbons disabled');

         if (cb) {
            cb.call(this);
         }
      }, function(stanza) {
         jsxc.warn('Could not disable carbons', stanza);
      });
   },

   /**
    * Enable/Disable carbons depending on options key.
    *
    * @memberOf jsxc.xmpp.carbons
    * @param err error message
    */
   refresh: function(err) {
      if (err === false) {
         return;
      }

      if (jsxc.options.get('carbons').enable) {
         return jsxc.xmpp.carbons.enable();
      }

      return jsxc.xmpp.carbons.disable();
   }
};