changeset 2144:1d3f73e065e1

core, jp: component handling + client handling refactoring: - SàT can now handle components - plugin have now a "modes" key in PLUGIN_INFO where they declare if they can be used with clients and or components. They default to be client only. - components are really similar to clients, but with some changes in behaviour: * component has "entry point", which is a special plugin with a componentStart method, which is called just after component is connected * trigger end with a different suffixes (e.g. profileConnected vs profileConnectedComponent), so a plugin which manage both clients and components can have different workflow * for clients, only triggers of plugins handling client mode are launched * for components, only triggers of plugins needed in dependencies are launched. They all must handle component mode. * component have a sendHistory attribute (False by default) which can be set to True to allow saving sent messages into history * for convenience, "client" is still used in method even if it can now be a component * a new "component" boolean attribute tells if we have a component or a client * components have to add themselve Message protocol * roster and presence protocols are not added for components * component default port is 5347 (which is Prosody's default port) - asyncCreateProfile has been renamed for profileCreate, both to follow new naming convention and to prepare the transition to fully asynchronous bridge - createProfile has a new "component" attribute. When used to create a component, it must be set to a component entry point - jp: added --component argument to profile/create - disconnect bridge method is now asynchronous, this way frontends can know when disconnection is finished - new PI_* constants for PLUGIN_INFO values (not used everywhere yet) - client/component connection workflow has been moved to their classes instead of being a host methods - host.messageSend is now client.sendMessage, and former client.sendMessage is now client.sendMessageData. - identities are now handled in client.identities list, so it can be updated dynamically by plugins (in the future, frontends should be able to update them too through bridge) - profileConnecting* profileConnected* profileDisconnected* and getHandler now all use client instead of profile
author Goffi <goffi@goffi.org>
date Sun, 12 Feb 2017 17:55:43 +0100 (2017-02-12)
parents c3cac21157d4
children 33c8c4973743
files frontends/src/bridge/dbus_bridge.py frontends/src/jp/cmd_profile.py frontends/src/primitivus/profile_manager.py frontends/src/quick_frontend/quick_profile_manager.py src/bridge/bridge_constructor/bridge_template.ini src/bridge/dbus_bridge.py src/core/constants.py src/core/sat_main.py src/core/xmpp.py src/memory/memory.py src/memory/params.py src/memory/sqlite.py src/plugins/plugin_exp_command_export.py src/plugins/plugin_exp_lang_detect.py src/plugins/plugin_exp_parrot.py src/plugins/plugin_misc_account.py src/plugins/plugin_misc_file.py src/plugins/plugin_misc_groupblog.py src/plugins/plugin_misc_ip.py src/plugins/plugin_misc_maildir.py src/plugins/plugin_misc_room_game.py src/plugins/plugin_misc_smtp.py src/plugins/plugin_misc_text_commands.py src/plugins/plugin_misc_upload.py src/plugins/plugin_misc_welcome.py src/plugins/plugin_sec_otr.py src/plugins/plugin_xep_0020.py src/plugins/plugin_xep_0033.py src/plugins/plugin_xep_0045.py src/plugins/plugin_xep_0047.py src/plugins/plugin_xep_0048.py src/plugins/plugin_xep_0050.py src/plugins/plugin_xep_0054.py src/plugins/plugin_xep_0059.py src/plugins/plugin_xep_0060.py src/plugins/plugin_xep_0065.py src/plugins/plugin_xep_0070.py src/plugins/plugin_xep_0071.py src/plugins/plugin_xep_0085.py src/plugins/plugin_xep_0095.py src/plugins/plugin_xep_0115.py src/plugins/plugin_xep_0166.py src/plugins/plugin_xep_0184.py src/plugins/plugin_xep_0203.py src/plugins/plugin_xep_0231.py src/plugins/plugin_xep_0234.py src/plugins/plugin_xep_0249.py src/plugins/plugin_xep_0260.py src/plugins/plugin_xep_0261.py src/plugins/plugin_xep_0280.py src/plugins/plugin_xep_0297.py src/plugins/plugin_xep_0300.py src/plugins/plugin_xep_0313.py src/plugins/plugin_xep_0334.py src/plugins/plugin_xep_0363.py
diffstat 55 files changed, 839 insertions(+), 523 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/dbus_bridge.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/frontends/src/bridge/dbus_bridge.py	Sun Feb 12 17:55:43 2017 +0100
@@ -164,15 +164,6 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.addContact(entity_jid, profile_key, **kwargs)
 
-    def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
-        if callback is None:
-            error_handler = None
-        else:
-            if errback is None:
-                errback = log.error
-            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
-        return self.db_core_iface.asyncCreateProfile(profile, password, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
-
     def asyncDeleteProfile(self, profile, callback=None, errback=None):
         if callback is None:
             error_handler = None
@@ -243,12 +234,7 @@
             if errback is None:
                 errback = log.error
             error_handler = lambda err:errback(dbus_to_bridge_exception(err))
-        kwargs={}
-        if callback is not None:
-            kwargs['timeout'] = const_TIMEOUT
-            kwargs['reply_handler'] = callback
-            kwargs['error_handler'] = error_handler
-        return self.db_core_iface.disconnect(profile_key, **kwargs)
+        return self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
     def getConfig(self, section, name, callback=None, errback=None):
         if callback is None:
@@ -560,6 +546,15 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.paramsRegisterApp(xml, security_limit, app, **kwargs)
 
+    def profileCreate(self, profile, password='', component='', callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.profileCreate(profile, password, component, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
+
     def profileIsSessionStarted(self, profile_key="@DEFAULT@", callback=None, errback=None):
         if callback is None:
             error_handler = None
--- a/frontends/src/jp/cmd_profile.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/frontends/src/jp/cmd_profile.py	Sun Feb 12 17:55:43 2017 +0100
@@ -125,6 +125,8 @@
         self.parser.add_argument('-j', '--jid', type=str, help=_('the jid of the profile'))
         self.parser.add_argument('-x', '--xmpp-password', type=str, help=_('the password of the XMPP account (use profile password if not specified)'),
                                  metavar='PASSWORD')
+        self.parser.add_argument('-C', '--component', type=base.unicode_decoder, default='',
+                                 help=_(u'set to component import name (entry point) if this is a component'))
 
     def _session_started(self, dummy):
         if self.args.jid:
@@ -142,7 +144,7 @@
         if self.args.profile in self.host.bridge.getProfilesList():
             log.error("Profile %s already exists." % self.args.profile)
             self.host.quit(1)
-        self.host.bridge.asyncCreateProfile(self.args.profile, self.args.password, callback=self._profile_created, errback=None)
+        self.host.bridge.profileCreate(self.args.profile, self.args.password, self.args.component, callback=self._profile_created, errback=None)
 
 
 class ProfileModify(base.CommandBase):
--- a/frontends/src/primitivus/profile_manager.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/frontends/src/primitivus/profile_manager.py	Sun Feb 12 17:55:43 2017 +0100
@@ -88,7 +88,7 @@
     def newProfile(self, button, edit):
         """Create the profile"""
         name = edit.get_edit_text()
-        self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
+        self.host.bridge.createProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
 
     def newProfileCreated(self, profile):
         # new profile will be selected, and a selected profile assume the session is started
--- a/frontends/src/quick_frontend/quick_profile_manager.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/frontends/src/quick_frontend/quick_profile_manager.py	Sun Feb 12 17:55:43 2017 +0100
@@ -134,7 +134,7 @@
     def _getErrorMessage(self, reason):
         """Return an error message corresponding to profile creation error
 
-        @param reason (str): reason as returned by asyncCreateProfile
+        @param reason (str): reason as returned by createProfile
         @return (unicode): human readable error message
         """
         if reason == "ConflictError":
--- a/src/bridge/bridge_constructor/bridge_template.ini	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Sun Feb 12 17:55:43 2017 +0100
@@ -236,20 +236,23 @@
 doc_return=dictionary with jids as keys and dictionary of asked key as values
  if key doesn't exist for a jid, the resulting dictionary will not have it
 
-[asyncCreateProfile]
+[profileCreate]
 async=
 type=method
 category=core
-sig_in=ss
+sig_in=sss
 sig_out=
 param_1_default=''
+param_2_default=''
 doc=Create a new profile
 doc_param_0=%(doc_profile)s
 doc_param_1=password: password of the profile
+doc_param_2=component: set to component entry point if it is a component, else use empty string
 doc_return=callback is called when profile actually exists in database and memory
 errback is called with error constant as parameter:
  - ConflictError: the profile name already exists
  - CancelError: profile creation canceled
+ - NotFound: component entry point is not available
 
 [asyncDeleteProfile]
 async=
@@ -308,6 +311,7 @@
 doc_param_0=%(doc_profile_key)s
 
 [disconnect]
+async=
 type=method
 category=core
 sig_in=s
--- a/src/bridge/dbus_bridge.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/bridge/dbus_bridge.py	Sun Feb 12 17:55:43 2017 +0100
@@ -204,12 +204,6 @@
         return self._callback("addContact", unicode(entity_jid), unicode(profile_key))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
-                         in_signature='ss', out_signature='',
-                         async_callbacks=('callback', 'errback'))
-    def asyncCreateProfile(self, profile, password='', callback=None, errback=None):
-        return self._callback("asyncCreateProfile", unicode(profile), unicode(password), callback=callback, errback=errback)
-
-    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='s', out_signature='',
                          async_callbacks=('callback', 'errback'))
     def asyncDeleteProfile(self, profile, callback=None, errback=None):
@@ -253,9 +247,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='s', out_signature='',
-                         async_callbacks=None)
-    def disconnect(self, profile_key="@DEFAULT@"):
-        return self._callback("disconnect", unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("disconnect", unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='s',
@@ -408,6 +402,12 @@
         return self._callback("paramsRegisterApp", unicode(xml), security_limit, unicode(app))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='sss', out_signature='',
+                         async_callbacks=('callback', 'errback'))
+    def profileCreate(self, profile, password='', component='', callback=None, errback=None):
+        return self._callback("profileCreate", unicode(profile), unicode(password), unicode(component), callback=callback, errback=errback)
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='s', out_signature='b',
                          async_callbacks=None)
     def profileIsSessionStarted(self, profile_key="@DEFAULT@"):
--- a/src/core/constants.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/core/constants.py	Sun Feb 12 17:55:43 2017 +0100
@@ -47,6 +47,8 @@
     XMPP_C2S_PORT = 5222
     XMPP_KEEP_ALIFE = 180
     XMPP_MAX_RETRIES = 2
+    # default port used on Prosody, may differ on other servers
+    XMPP_COMPONENT_PORT = 5347
 
 
     ## Parameters ##
@@ -171,6 +173,13 @@
 
     ## Plugins ##
 
+    # PLUGIN_INFO keys
+    # XXX: we use PI instead of PLUG_INFO which would normally be used
+    #      to make the header more readable
+    PI_IMPORT_NAME = u'import_name'
+    PI_DEPENDENCIES = u'dependencies'
+    PI_RECOMMENDATIONS = u'recommendations'
+
     # Types
     PLUG_TYPE_XEP = "XEP"
     PLUG_TYPE_MISC = "MISC"
@@ -178,7 +187,12 @@
     PLUG_TYPE_SEC = "SEC"
     PLUG_TYPE_SYNTAXE = "SYNTAXE"
     PLUG_TYPE_BLOG = "BLOG"
+    PLUG_TYPE_ENTRY_POINT = "ENTRY_POINT"
 
+    # Modes
+    PLUG_MODE_CLIENT = "client"
+    PLUG_MODE_COMPONENT = "component"
+    PLUG_MODE_DEFAULT = (PLUG_MODE_CLIENT,)
 
     # names of widely used plugins
     TEXT_CMDS = 'TEXT-COMMANDS'
--- a/src/core/sat_main.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/core/sat_main.py	Sun Feb 12 17:55:43 2017 +0100
@@ -22,7 +22,6 @@
 from twisted.application import service
 from twisted.internet import defer
 from twisted.words.protocols.jabber import jid
-from twisted.words.xish import domish
 from twisted.internet import reactor
 from wokkel.xmppim import RosterItem
 from sat.core import xmpp
@@ -41,7 +40,6 @@
 import sys
 import os.path
 import uuid
-import time
 
 try:
     from collections import OrderedDict # only available from python 2.7
@@ -81,7 +79,7 @@
         self.bridge.register_method("getProfilesList", self.memory.getProfilesList)
         self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile))
         self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData)
-        self.bridge.register_method("asyncCreateProfile", self.memory.asyncCreateProfile)
+        self.bridge.register_method("profileCreate", self.memory.createProfile)
         self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile)
         self.bridge.register_method("profileStartSession", self.memory.startSession)
         self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted)
@@ -156,8 +154,20 @@
         self.initialised.callback(None)
         log.info(_(u"Backend is ready"))
 
+    def _unimport_plugin(self, plugin_path):
+        """remove a plugin from sys.modules if it is there"""
+        try:
+            del sys.modules[plugin_path]
+        except KeyError:
+            pass
+
     def _import_plugins(self):
         """Import all plugins found in plugins directory"""
+        # FIXME: module imported but cancelled should be deleted
+        # TODO: make this more generic and reusable in tools.common
+        # FIXME: should use imp
+        # TODO: do not import all plugins if no needed: component plugins are not needed if we
+        #       just use a client, and plugin blacklisting should be possible in sat.conf
         plugins_path = os.path.dirname(sat.plugins.__file__)
         plugin_glob = "plugin*." + C.PLUGIN_EXT
         plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))]
@@ -167,23 +177,34 @@
             try:
                 __import__(plugin_path)
             except exceptions.MissingModule as e:
-                try:
-                    del sys.modules[plugin_path]
-                except KeyError:
-                    pass
+                self._unimport_plugin(plugin_path)
                 log.warning(u"Can't import plugin [{path}] because of an unavailale third party module:\n{msg}".format(
                     path=plugin_path, msg=e))
                 continue
             except exceptions.CancelError as e:
                 log.info(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e))
+                self._unimport_plugin(plugin_path)
                 continue
             except Exception as e:
                 import traceback
                 log.error(_(u"Can't import plugin [{path}]:\n{error}").format(path=plugin_path, error=traceback.format_exc()))
+                self._unimport_plugin(plugin_path)
                 continue
             mod = sys.modules[plugin_path]
             plugin_info = mod.PLUGIN_INFO
             import_name = plugin_info['import_name']
+
+            plugin_modes = plugin_info[u'modes'] = set(plugin_info.setdefault(u"modes", C.PLUG_MODE_DEFAULT))
+
+            # if the plugin is an entry point, it must work in component mode
+            if plugin_info[u'type'] == C.PLUG_TYPE_ENTRY_POINT:
+                # if plugin is an entrypoint, we cache it
+                if C.PLUG_MODE_COMPONENT not in plugin_modes:
+                    log.error(_(u"{type} type must be used with {mode} mode, ignoring plugin").format(
+                        type = C.PLUG_TYPE_ENTRY_POINT, mode = C.PLUG_MODE_COMPONENT))
+                    self._unimport_plugin(plugin_path)
+                    continue
+
             if import_name in plugins_to_import:
                 log.error(_(u"Name conflict for import name [{import_name}], can't import plugin [{name}]").format(**plugin_info))
                 continue
@@ -243,6 +264,8 @@
             self.plugins[import_name].is_handler = True
         else:
             self.plugins[import_name].is_handler = False
+        # we keep metadata as a Class attribute
+        self.plugins[import_name]._info = plugin_info
         #TODO: test xmppclient presence and register handler parent
 
     def pluginsUnload(self):
@@ -268,12 +291,14 @@
         return self.connect(profile, password, options)
 
     def connect(self, profile, password='', options=None, max_retries=C.XMPP_MAX_RETRIES):
-        """Retrieve the individual parameters, authenticate the profile
+        """Connect a profile (i.e. connect client.component to XMPP server)
+
+        Retrieve the individual parameters, authenticate the profile
         and initiate the connection to the associated XMPP server.
-
         @param profile: %(doc_profile)s
         @param password (string): the SàT profile password
-        @param options (dict): connection options
+        @param options (dict): connection options. Key can be:
+            -
         @param max_retries (int): max number of connection retries
         @return (D(bool)):
             - True if the XMPP connection was already established
@@ -282,100 +307,32 @@
         """
         if options is None:
             options={}
-        def connectXMPPClient(dummy=None):
+        def connectProfile(dummy=None):
             if self.isConnected(profile):
                 log.info(_("already connected !"))
                 return True
-            d = self._connectXMPPClient(profile, max_retries)
+
+            if self.memory.isComponent(profile):
+                d = xmpp.SatXMPPComponent.startConnection(self, profile, max_retries)
+            else:
+                d = xmpp.SatXMPPClient.startConnection(self, profile, max_retries)
             return d.addCallback(lambda dummy: False)
 
         d = self.memory.startSession(password, profile)
-        d.addCallback(connectXMPPClient)
+        d.addCallback(connectProfile)
         return d
 
-    @defer.inlineCallbacks
-    def _connectXMPPClient(self, profile, max_retries):
-        """This part is called from connect when we have loaded individual parameters from memory"""
-        try:
-            port = int(self.memory.getParamA(C.FORCE_PORT_PARAM, "Connection", profile_key=profile))
-        except ValueError:
-            log.debug(_("Can't parse port value, using default value"))
-            port = None  # will use default value 5222 or be retrieved from a DNS SRV record
-
-        password = yield self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
-        current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
-            jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)),
-            password, self.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile),
-            port, max_retries)
-
-        current.messageProt = xmpp.SatMessageProtocol(self)
-        current.messageProt.setHandlerParent(current)
-
-        current.roster = xmpp.SatRosterProtocol(self)
-        current.roster.setHandlerParent(current)
-
-        current.presence = xmpp.SatPresenceProtocol(self)
-        current.presence.setHandlerParent(current)
-
-        current.fallBack = xmpp.SatFallbackHandler(self)
-        current.fallBack.setHandlerParent(current)
-
-        current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
-                                                        self.full_version)
-        current.versionHandler.setHandlerParent(current)
-
-        current.identityHandler = xmpp.SatIdentityHandler()
-        current.identityHandler.setHandlerParent(current)
-
-        log.debug(_("setting plugins parents"))
-
-        plugin_conn_cb = []
-        for plugin in self.plugins.iteritems():
-            if plugin[1].is_handler:
-                plugin[1].getHandler(profile).setHandlerParent(current)
-            connected_cb = getattr(plugin[1], "profileConnected", None) # profile connected is called after client is ready and roster is got
-            if connected_cb:
-                plugin_conn_cb.append((plugin[0], connected_cb))
-            try:
-                yield plugin[1].profileConnecting(profile) # profile connecting is called before actually starting client
-            except AttributeError:
-                pass
-
-        current.startService()
-
-        yield current.getConnectionDeferred()
-        yield current.roster.got_roster  # we want to be sure that we got the roster
-
-        # Call profileConnected callback for all plugins, and print error message if any of them fails
-        conn_cb_list = []
-        for dummy, callback in plugin_conn_cb:
-            conn_cb_list.append(defer.maybeDeferred(callback, profile))
-        list_d = defer.DeferredList(conn_cb_list)
-
-        def logPluginResults(results):
-            all_succeed = all([success for success, result in results])
-            if not all_succeed:
-                log.error(_(u"Plugins initialisation error"))
-                for idx, (success, result) in enumerate(results):
-                    if not success:
-                        log.error(u"error (plugin %(name)s): %(failure)s" %
-                                  {'name': plugin_conn_cb[idx][0], 'failure': result})
-
-        yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
-        # TODO: mesure launch time of each plugin
-
     def disconnect(self, profile_key):
         """disconnect from jabber server"""
+        # FIXME: client should not be deleted if only disconnected
+        #        it shoud be deleted only when session is finished
         if not self.isConnected(profile_key):
-            log.info(_("not connected !"))
-            return
-        profile = self.memory.getProfileName(profile_key)
-        log.info(_("Disconnecting..."))
-        self.profiles[profile].stopService()
-        for plugin in self.plugins.iteritems():
-            disconnected_cb = getattr(plugin[1], "profileDisconnected", None)
-            if disconnected_cb:
-                disconnected_cb(profile)
+            # isConnected is checked here and not on client
+            # because client is deleted when session is ended
+            log.info(_(u"not connected !"))
+            return defer.succeed(None)
+        client = self.getClient(profile_key)
+        return client.entityDisconnect()
 
     def getFeatures(self, profile_key=C.PROF_KEY_NONE):
         """Get available features
@@ -439,14 +396,17 @@
         client = self.getClient(profile_key)
         return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)]
 
-    def purgeClient(self, profile):
-        """Remove reference to a profile client and purge cache
-        the garbage collector can then free the memory"""
+    def purgeEntity(self, profile):
+        """Remove reference to a profile client/component and purge cache
+
+        the garbage collector can then free the memory
+        """
         try:
             del self.profiles[profile]
         except KeyError:
             log.error(_("Trying to remove reference to a client not referenced"))
-        self.memory.purgeProfileSession(profile)
+        else:
+            self.memory.purgeProfileSession(profile)
 
     def startService(self):
         log.info(u"Salut à toi ô mon frère !")
@@ -515,6 +475,14 @@
         """
         return unicode(self.memory.getConfig(section, name, ''))
 
+    def logErrback(self, failure_):
+        """generic errback logging
+
+        can be used as last errback to show unexpected error
+        """
+        log.error(_(u"Unexpected error: {}".format(failure_)))
+        return failure_
+
     ## Client management ##
 
     def setParam(self, name, value, category, security_limit, profile_key):
@@ -537,168 +505,11 @@
 
     ## XMPP methods ##
 
-    def generateMessageXML(self, data):
-        """Generate <message/> stanza from message data
-
-        @param data(dict): message data
-            domish element will be put in data['xml']
-            following keys are needed:
-                - from
-                - to
-                - uid: can be set to '' if uid attribute is not wanted
-                - message
-                - type
-                - subject
-                - extra
-        @return (dict) message data
-        """
-        data['xml'] = message_elt = domish.Element((None, 'message'))
-        message_elt["to"] = data["to"].full()
-        message_elt["from"] = data['from'].full()
-        message_elt["type"] = data["type"]
-        if data['uid']: # key must be present but can be set to ''
-                        # by a plugin to avoid id on purpose
-            message_elt['id'] = data['uid']
-        for lang, subject in data["subject"].iteritems():
-            subject_elt = message_elt.addElement("subject", content=subject)
-            if lang:
-                subject_elt[(C.NS_XML, 'lang')] = lang
-        for lang, message in data["message"].iteritems():
-            body_elt = message_elt.addElement("body", content=message)
-            if lang:
-                body_elt[(C.NS_XML, 'lang')] = lang
-        try:
-            thread = data['extra']['thread']
-        except KeyError:
-            if 'thread_parent' in data['extra']:
-                raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
-        else:
-            thread_elt = message_elt.addElement("thread", content=thread)
-            try:
-                thread_elt["parent"] = data["extra"]["thread_parent"]
-            except KeyError:
-                pass
-        return data
-
     def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE):
         client = self.getClient(profile_key)
         to_jid = jid.JID(to_jid_s)
         #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way
-        return self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
-
-    def messageSend(self, client, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
-        """Send a message to an entity
-
-        @param to_jid(jid.JID): destinee of the message
-        @param message(dict): message body, key is the language (use '' when unknown)
-        @param subject(dict): message subject, key is the language (use '' when unknown)
-        @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
-            - auto: for automatic type detection
-            - info: for information ("info_type" can be specified in extra)
-        @param extra(dict, None): extra data. Key can be:
-            - info_type: information type, can be
-                TODO
-        @param uid(unicode, None): unique id:
-            should be unique at least in this XMPP session
-            if None, an uuid will be generated
-        @param no_trigger (bool): if True, messageSend trigger will no be used
-            useful when a message need to be sent without any modification
-        """
-        profile = client.profile
-        if subject is None:
-            subject = {}
-        if extra is None:
-            extra = {}
-        data = {  # dict is similar to the one used in client.onMessage
-            "from": client.jid,
-            "to": to_jid,
-            "uid": uid or unicode(uuid.uuid4()),
-            "message": message,
-            "subject": subject,
-            "type": mess_type,
-            "extra": extra,
-            "timestamp": time.time(),
-        }
-        pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
-        post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
-
-        if data["type"] == "auto":
-            # we try to guess the type
-            if data["subject"]:
-                data["type"] = 'normal'
-            elif not data["to"].resource:  # if to JID has a resource, the type is not 'groupchat'
-                # we may have a groupchat message, we check if the we know this jid
-                try:
-                    entity_type = self.memory.getEntityData(data["to"], ['type'], profile)["type"]
-                    #FIXME: should entity_type manage resources ?
-                except (exceptions.UnknownEntityError, KeyError):
-                    entity_type = "contact"
-
-                if entity_type == "chatroom":
-                    data["type"] = 'groupchat'
-                else:
-                    data["type"] = 'chat'
-            else:
-                data["type"] == 'chat'
-            data["type"] == "chat" if data["subject"] else "normal"
-
-        # FIXME: send_only is used by libervia's OTR plugin to avoid
-        #        the triggers from frontend, and no_trigger do the same
-        #        thing internally, this could be unified
-        send_only = data['extra'].get('send_only', False)
-
-        if not no_trigger and not send_only:
-            if not self.trigger.point("messageSend", client, data, pre_xml_treatments, post_xml_treatments):
-                return defer.succeed(None)
-
-        log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
-
-        pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
-        pre_xml_treatments.chainDeferred(post_xml_treatments)
-        post_xml_treatments.addCallback(client.sendMessage)
-        if send_only:
-            log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
-        else:
-            post_xml_treatments.addCallback(self.messageAddToHistory, client)
-            post_xml_treatments.addCallback(self.messageSendToBridge, client)
-            post_xml_treatments.addErrback(self._cancelErrorTrap)
-        pre_xml_treatments.callback(data)
-        return pre_xml_treatments
-
-    def _cancelErrorTrap(self, failure):
-        """A message sending can be cancelled by a plugin treatment"""
-        failure.trap(exceptions.CancelError)
-
-    def messageAddToHistory(self, data, client):
-        """Store message into database (for local history)
-
-        @param data: message data dictionnary
-        @param client: profile's client
-        """
-        if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
-            # we don't add groupchat message to history, as we get them back
-            # and they will be added then
-            if data[u'message'] or data[u'subject']: # we need a message to store
-                self.memory.addToHistory(client, data)
-            else:
-               log.warning(u"No message found") # empty body should be managed by plugins before this point
-        return data
-
-    def messageSendToBridge(self, data, client):
-        """Send message to bridge, so frontends can display it
-
-        @param data: message data dictionnary
-        @param client: profile's client
-        """
-        if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
-            # we don't send groupchat message to bridge, as we get them back
-            # and they will be added the
-            if data[u'message'] or data[u'subject']: # we need a message to send something
-                # We send back the message, so all frontends are aware of it
-                self.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=client.profile)
-            else:
-               log.warning(_(u"No message found"))
-        return data
+        return client.sendMessage(to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
 
     def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE):
         return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key)
--- a/src/core/xmpp.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/core/xmpp.py	Sun Feb 12 17:55:43 2017 +0100
@@ -28,6 +28,7 @@
 from twisted.words.xish import domish
 from twisted.python import failure
 from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel
+from wokkel import component
 from wokkel import delay
 from sat.core.log import getLogger
 log = getLogger(__name__)
@@ -39,40 +40,229 @@
 import sys
 
 
-class SatXMPPClient(wokkel_client.XMPPClient):
-    implements(iwokkel.IDisco)
+class SatXMPPEntity(object):
+    """Common code for Client and Component"""
 
-    def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
-        # XXX: DNS SRV records are checked when the host is not specified.
-        # If no SRV record is found, the host is directly extracted from the JID.
-        self.started = time.time()
-        if sys.platform == "android":
-            # FIXME: temporary hack as SRV is not working on android
-            # TODO: remove this hack and fix SRV
-            log.info(u"FIXME: Android hack, ignoring SRV")
-            host = user_jid.host
-            hosts_map = host_app.memory.getConfig("android", "hosts_dict", {})
-            if host in hosts_map:
-                log.info(u"using {host_to_use} for host {host_ori} as requested in config".format(
-                    host_ori = host,
-                    host_to_use = hosts_map[host]))
-                host = hosts_map[host]
-        wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
+    def __init__(self, host_app, profile, max_retries):
+
         self.factory.clientConnectionLost = self.connectionLost
         self.factory.maxRetries = max_retries
-        self.__connected = False
+        # when self._connected is None, we are not connected
+        # else, it's a deferred which fire on disconnection
+        self._connected = None
         self.profile = profile
         self.host_app = host_app
         self.cache = cache.Cache(host_app, profile)
-        self._mess_id_uid = {} # map from message id to uid use in history. Key: (full_jid,message_id) Value: uid
+        self._mess_id_uid = {} # map from message id to uid used in history. Key: (full_jid,message_id) Value: uid
         self.conn_deferred = defer.Deferred()
-        self._progress_cb = {}  # callback called when a progress is requested (key = progress id)
-        self.actions = {} # used to keep track of actions for retrieval (key = action_id)
+
+    ## initialisation ##
+
+    @defer.inlineCallbacks
+    def _callConnectionTriggers(self):
+        """Call conneting trigger prepare connected trigger
+
+        @param plugins(iterable): plugins to use
+        @return (list[object, callable]): plugin to trigger tuples with:
+            - plugin instance
+            - profileConnected* triggers (to call after connection)
+        """
+        plugin_conn_cb = []
+        for plugin in self._getPluginsList():
+            # we check if plugin handle client mode
+            if plugin.is_handler:
+                plugin.getHandler(self).setHandlerParent(self)
+
+            # profileConnecting/profileConnected methods handling
+
+            # profile connecting is called right now (before actually starting client)
+            connecting_cb = getattr(plugin, "profileConnecting" + self.trigger_suffix, None)
+            if connecting_cb is not None:
+                yield connecting_cb(self)
+
+            # profile connected is called after client is ready and roster is got
+            connected_cb = getattr(plugin, "profileConnected" + self.trigger_suffix, None)
+            if connected_cb is not None:
+                plugin_conn_cb.append((plugin, connected_cb))
+
+        defer.returnValue(plugin_conn_cb)
+
+    def _getPluginsList(self):
+        """Return list of plugin to use
+
+        need to be implemented by subclasses
+        this list is used to call profileConnect* triggers
+        @return(iterable[object]): plugins to use
+        """
+        raise NotImplementedError
+
+    def _createSubProtocols(self):
+        return
+
+    def entityConnected(self):
+        """Called once connection is done
+
+        may return a Deferred, to perform initialisation tasks
+        """
+        return
+
+    @classmethod
+    @defer.inlineCallbacks
+    def startConnection(cls, host, profile, max_retries):
+        """instantiate the entity and start the connection"""
+        # FIXME: reconnection doesn't seems to be handled correclty (client is deleted then recreated from scrash
+        #        most of methods called here should be called once on first connection (e.g. adding subprotocols)
+        #        but client should not be deleted except if session is finished (independently of connection/deconnection
+        #
+        try:
+            port = int(host.memory.getParamA(C.FORCE_PORT_PARAM, "Connection", profile_key=profile))
+        except ValueError:
+            log.debug(_("Can't parse port value, using default value"))
+            port = None  # will use default value 5222 or be retrieved from a DNS SRV record
+
+        password = yield host.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
+        entity = host.profiles[profile] = cls(host, profile,
+            jid.JID(host.memory.getParamA("JabberID", "Connection", profile_key=profile)),
+            password, host.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile) or None,
+            port, max_retries)
+
+        entity._createSubProtocols()
+
+        entity.fallBack = SatFallbackHandler(host)
+        entity.fallBack.setHandlerParent(entity)
+
+        entity.versionHandler = SatVersionHandler(C.APP_NAME_FULL,
+                                                  host.full_version)
+        entity.versionHandler.setHandlerParent(entity)
+
+        entity.identityHandler = SatIdentityHandler()
+        entity.identityHandler.setHandlerParent(entity)
+
+        log.debug(_("setting plugins parents"))
+
+        plugin_conn_cb = yield entity._callConnectionTriggers()
+
+        entity.startService()
+
+        yield entity.getConnectionDeferred()
+
+        yield defer.maybeDeferred(entity.entityConnected)
+
+        # Call profileConnected callback for all plugins, and print error message if any of them fails
+        conn_cb_list = []
+        for dummy, callback in plugin_conn_cb:
+            conn_cb_list.append(defer.maybeDeferred(callback, entity))
+        list_d = defer.DeferredList(conn_cb_list)
+
+        def logPluginResults(results):
+            all_succeed = all([success for success, result in results])
+            if not all_succeed:
+                log.error(_(u"Plugins initialisation error"))
+                for idx, (success, result) in enumerate(results):
+                    if not success:
+                        log.error(u"error (plugin %(name)s): %(failure)s" %
+                                  {'name': plugin_conn_cb[idx][0]._info['import_name'], 'failure': result})
+
+        yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
+        # TODO: mesure launch time of each plugin
 
     def getConnectionDeferred(self):
         """Return a deferred which fire when the client is connected"""
         return self.conn_deferred
 
+    def _disconnectionCb(self, dummy):
+        self._connected = None
+
+    def _disconnectionEb(self, failure_):
+        log.error(_(u"Error while disconnecting: {}".format(failure_)))
+
+    def _authd(self, xmlstream):
+        if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
+            return
+        super(SatXMPPEntity, self)._authd(xmlstream)
+
+        # the following Deferred is used to know when we are connected
+        # so we need to be set it to None when connection is lost
+        self._connected = defer.Deferred()
+        self._connected.addCallback(self._cleanConnection)
+        self._connected.addCallback(self._disconnectionCb)
+        self._connected.addErrback(self._disconnectionEb)
+
+        log.info(_("********** [%s] CONNECTED **********") % self.profile)
+        self.streamInitialized()
+        self.host_app.bridge.connected(self.profile, unicode(self.jid))  # we send the signal to the clients
+
+    def _finish_connection(self, dummy):
+        self.conn_deferred.callback(None)
+
+    def streamInitialized(self):
+        """Called after _authd"""
+        log.debug(_(u"XML stream is initialized"))
+        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  # Needed to avoid disconnection (specially with openfire)
+        self.keep_alife.start(C.XMPP_KEEP_ALIFE)
+
+        self.disco = SatDiscoProtocol(self)
+        self.disco.setHandlerParent(self)
+        self.discoHandler = disco.DiscoHandler()
+        self.discoHandler.setHandlerParent(self)
+        disco_d = defer.succeed(None)
+
+        if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
+            return
+
+        disco_d.addCallback(self._finish_connection)
+
+    def initializationFailed(self, reason):
+        log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
+        self.conn_deferred.errback(reason.value)
+        try:
+            super(SatXMPPEntity, self).initializationFailed(reason)
+        except:
+            # we already chained an errback, no need to raise an exception
+            pass
+
+    ## connection ##
+
+    def connectionLost(self, connector, reason):
+        try:
+            self.keep_alife.stop()
+        except AttributeError:
+            log.debug(_("No keep_alife"))
+        if self._connected is not None:
+            self.host_app.bridge.disconnected(self.profile)  # we send the signal to the clients
+            self._connected.callback(None)
+            self.host_app.purgeEntity(self.profile)  # and we remove references to this client
+            log.info(_("********** [%s] DISCONNECTED **********") % self.profile)
+        if not self.conn_deferred.called:
+            # FIXME: real error is not gotten here (e.g. if jid is not know by Prosody,
+            #        we should have the real error)
+            self.conn_deferred.errback(error.StreamError(u"Server unexpectedly closed the connection"))
+
+    @defer.inlineCallbacks
+    def _cleanConnection(self, dummy):
+        """method called on disconnection
+
+        used to call profileDisconnected* triggers
+        """
+        trigger_name = "profileDisconnected" + self.trigger_suffix
+        for plugin in self._getPluginsList():
+            disconnected_cb = getattr(plugin, trigger_name, None)
+            if disconnected_cb is not None:
+                yield disconnected_cb(self)
+
+    def isConnected(self):
+        return self._connected is not None
+
+    def entityDisconnect(self):
+        log.info(_(u"Disconnecting..."))
+        self.stopService()
+        if self._connected is not None:
+            return self._connected
+        else:
+            return defer.succeed(None)
+
+    ## sending ##
+
     def IQ(self, type_=u'set', timeout=None):
         """shortcut to create an IQ element managing deferred
 
@@ -94,6 +284,223 @@
         iq_error_elt = error.StanzaError(condition).toResponse(iq_elt)
         self.xmlstream.send(iq_error_elt)
 
+    def generateMessageXML(self, data):
+        """Generate <message/> stanza from message data
+
+        @param data(dict): message data
+            domish element will be put in data['xml']
+            following keys are needed:
+                - from
+                - to
+                - uid: can be set to '' if uid attribute is not wanted
+                - message
+                - type
+                - subject
+                - extra
+        @return (dict) message data
+        """
+        data['xml'] = message_elt = domish.Element((None, 'message'))
+        message_elt["to"] = data["to"].full()
+        message_elt["from"] = data['from'].full()
+        message_elt["type"] = data["type"]
+        if data['uid']: # key must be present but can be set to ''
+                        # by a plugin to avoid id on purpose
+            message_elt['id'] = data['uid']
+        for lang, subject in data["subject"].iteritems():
+            subject_elt = message_elt.addElement("subject", content=subject)
+            if lang:
+                subject_elt[(C.NS_XML, 'lang')] = lang
+        for lang, message in data["message"].iteritems():
+            body_elt = message_elt.addElement("body", content=message)
+            if lang:
+                body_elt[(C.NS_XML, 'lang')] = lang
+        try:
+            thread = data['extra']['thread']
+        except KeyError:
+            if 'thread_parent' in data['extra']:
+                raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
+        else:
+            thread_elt = message_elt.addElement("thread", content=thread)
+            try:
+                thread_elt["parent"] = data["extra"]["thread_parent"]
+            except KeyError:
+                pass
+        return data
+
+    def addPostXmlCallbacks(self, post_xml_treatments):
+        """Used to add class level callbacks at the end of the workflow
+
+        @param post_xml_treatments(D): the same Deferred as in sendMessage trigger
+        """
+        raise NotImplementedError
+
+    def sendMessage(self, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
+        """Send a message to an entity
+
+        @param to_jid(jid.JID): destinee of the message
+        @param message(dict): message body, key is the language (use '' when unknown)
+        @param subject(dict): message subject, key is the language (use '' when unknown)
+        @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
+            - auto: for automatic type detection
+            - info: for information ("info_type" can be specified in extra)
+        @param extra(dict, None): extra data. Key can be:
+            - info_type: information type, can be
+                TODO
+        @param uid(unicode, None): unique id:
+            should be unique at least in this XMPP session
+            if None, an uuid will be generated
+        @param no_trigger (bool): if True, sendMessage[suffix] trigger will no be used
+            useful when a message need to be sent without any modification
+        """
+        if subject is None:
+            subject = {}
+        if extra is None:
+            extra = {}
+        data = {  # dict is similar to the one used in client.onMessage
+            "from": self.jid,
+            "to": to_jid,
+            "uid": uid or unicode(uuid.uuid4()),
+            "message": message,
+            "subject": subject,
+            "type": mess_type,
+            "extra": extra,
+            "timestamp": time.time(),
+        }
+        pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
+        post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
+
+        if data["type"] == "auto":
+            # we try to guess the type
+            if data["subject"]:
+                data["type"] = 'normal'
+            elif not data["to"].resource:  # if to JID has a resource, the type is not 'groupchat'
+                # we may have a groupchat message, we check if the we know this jid
+                try:
+                    entity_type = self.host_app.memory.getEntityData(data["to"], ['type'], self.profile)["type"]
+                    #FIXME: should entity_type manage resources ?
+                except (exceptions.UnknownEntityError, KeyError):
+                    entity_type = "contact"
+
+                if entity_type == "chatroom":
+                    data["type"] = 'groupchat'
+                else:
+                    data["type"] = 'chat'
+            else:
+                data["type"] == 'chat'
+            data["type"] == "chat" if data["subject"] else "normal"
+
+        # FIXME: send_only is used by libervia's OTR plugin to avoid
+        #        the triggers from frontend, and no_trigger do the same
+        #        thing internally, this could be unified
+        send_only = data['extra'].get('send_only', False)
+
+        if not no_trigger and not send_only:
+            if not self.host_app.trigger.point("sendMessage" + self.trigger_suffix, self, data, pre_xml_treatments, post_xml_treatments):
+                return defer.succeed(None)
+
+        log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
+
+        pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
+        pre_xml_treatments.chainDeferred(post_xml_treatments)
+        post_xml_treatments.addCallback(self.sendMessageData)
+        if send_only:
+            log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
+        else:
+            self.addPostXmlCallbacks(post_xml_treatments)
+            post_xml_treatments.addErrback(self._cancelErrorTrap)
+            post_xml_treatments.addErrback(self.host_app.logErrback)
+        pre_xml_treatments.callback(data)
+        return pre_xml_treatments
+
+    def _cancelErrorTrap(self, failure):
+        """A message sending can be cancelled by a plugin treatment"""
+        failure.trap(exceptions.CancelError)
+
+    def messageAddToHistory(self, data):
+        """Store message into database (for local history)
+
+        @param data: message data dictionnary
+        @param client: profile's client
+        """
+        if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
+            # we don't add groupchat message to history, as we get them back
+            # and they will be added then
+            if data[u'message'] or data[u'subject']: # we need a message to store
+                self.host_app.memory.addToHistory(self, data)
+            else:
+               log.warning(u"No message found") # empty body should be managed by plugins before this point
+        return data
+
+    def messageSendToBridge(self, data):
+        """Send message to bridge, so frontends can display it
+
+        @param data: message data dictionnary
+        @param client: profile's client
+        """
+        if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
+            # we don't send groupchat message to bridge, as we get them back
+            # and they will be added the
+            if data[u'message'] or data[u'subject']: # we need a message to send something
+                # We send back the message, so all frontends are aware of it
+                self.host_app.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=self.profile)
+            else:
+               log.warning(_(u"No message found"))
+        return data
+
+
+class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
+    implements(iwokkel.IDisco)
+    trigger_suffix = ""
+    component = False
+
+    def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
+        # XXX: DNS SRV records are checked when the host is not specified.
+        # If no SRV record is found, the host is directly extracted from the JID.
+        self.started = time.time()
+
+        # Currently, we use "client/pc/Salut à Toi", but as
+        # SàT is multi-frontends and can be used on mobile devices, as a bot, with a web frontend,
+        # etc., we should implement a way to dynamically update identities through the bridge
+        self.identities = [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)]
+        if sys.platform == "android":
+            # FIXME: temporary hack as SRV is not working on android
+            # TODO: remove this hack and fix SRV
+            log.info(u"FIXME: Android hack, ignoring SRV")
+            host = user_jid.host
+            hosts_map = host_app.memory.getConfig("android", "hosts_dict", {})
+            if host in hosts_map:
+                log.info(u"using {host_to_use} for host {host_ori} as requested in config".format(
+                    host_ori = host,
+                    host_to_use = hosts_map[host]))
+                host = hosts_map[host]
+        wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
+        SatXMPPEntity.__init__(self, host_app, profile, max_retries)
+        self._progress_cb = {}  # callback called when a progress is requested (key = progress id)
+        self.actions = {} # used to keep track of actions for retrieval (key = action_id)
+
+    def _getPluginsList(self):
+        for p in self.host_app.plugins.itervalues():
+            if C.PLUG_MODE_CLIENT in p._info[u'modes']:
+                yield p
+
+    def _createSubProtocols(self):
+        self.messageProt = SatMessageProtocol(self.host_app)
+        self.messageProt.setHandlerParent(self)
+
+        self.roster = SatRosterProtocol(self.host_app)
+        self.roster.setHandlerParent(self)
+
+        self.presence = SatPresenceProtocol(self.host_app)
+        self.presence.setHandlerParent(self)
+
+    def entityConnected(self):
+        # we want to be sure that we got the roster
+        return self.roster.got_roster
+
+    def addPostXmlCallbacks(self, post_xml_treatments):
+        post_xml_treatments.addCallback(self.messageAddToHistory)
+        post_xml_treatments.addCallback(self.messageSendToBridge)
+
     def send(self, obj):
         # original send method accept string
         # but we restrict to domish.Element to make trigger treatments easier
@@ -109,7 +516,7 @@
         #     return
         super(SatXMPPClient, self).send(obj)
 
-    def sendMessage(self, mess_data):
+    def sendMessageData(self, mess_data):
         """Convenient method to send message data to stream
 
         This method will send mess_data[u'xml'] to stream, but a trigger is there
@@ -121,7 +528,7 @@
         # XXX: This is the last trigger before u"send" (last but one globally) for sending message.
         #      This is intented for e2e encryption which doesn't do full stanza encryption (e.g. OTR)
         #      This trigger point can't cancel the method
-        self.host_app.trigger.point("sendMessageFinish", self, mess_data)
+        self.host_app.trigger.point("sendMessageData", self, mess_data)
         self.send(mess_data[u'xml'])
         return mess_data
 
@@ -130,7 +537,7 @@
 
         This message will be an info message, not recorded in history.
         It can be used to give feedback of a command
-        @param to_jid(jid.Jid): destinee jid
+        @param to_jid(jid.JID): destinee jid
         @param message(unicode): message to send to frontends
         """
         self.host_app.bridge.messageNew(uid=unicode(uuid.uuid4()),
@@ -143,59 +550,108 @@
                                         extra={},
                                         profile=self.profile)
 
-    def _authd(self, xmlstream):
-        if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
-            return
-        wokkel_client.XMPPClient._authd(self, xmlstream)
-        self.__connected = True
-        log.info(_("********** [%s] CONNECTED **********") % self.profile)
-        self.streamInitialized()
-        self.host_app.bridge.connected(self.profile, unicode(self.jid))  # we send the signal to the clients
+    def _finish_connection(self, dummy):
+        self.roster.requestRoster()
+        self.presence.available()
+        super(SatXMPPClient, self)._finish_connection(dummy)
+
+
+class SatXMPPComponent(SatXMPPEntity, component.Component):
+    """XMPP component
+
+    This component are similar but not identical to clients.
+    An entry point plugin is launched after component is connected.
+    Component need to instantiate MessageProtocol itself
+    """
+    implements(iwokkel.IDisco)
+    trigger_suffix = "Component"  # used for to distinguish some trigger points set in SatXMPPEntity
+    component = True
+    sendHistory = False  # XXX: set to True from entry plugin to keep messages in history for received messages
+
+    def __init__(self, host_app, profile, component_jid, password, host=None, port=None, max_retries=C.XMPP_MAX_RETRIES):
+        self.started = time.time()
+        if port is None:
+            port = C.XMPP_COMPONENT_PORT
 
-    def streamInitialized(self):
-        """Called after _authd"""
-        log.debug(_("XML stream is initialized"))
-        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  # Needed to avoid disconnection (specially with openfire)
-        self.keep_alife.start(C.XMPP_KEEP_ALIFE)
+        ## entry point ##
+        entry_point = host_app.memory.getEntryPoint(profile)
+        try:
+            self.entry_plugin = host_app.plugins[entry_point]
+        except KeyError:
+            raise exceptions.NotFound(_(u"The requested entry point ({entry_point}) is not available").format(
+                entry_point = entry_point))
 
-        self.disco = SatDiscoProtocol(self)
-        self.disco.setHandlerParent(self)
-        self.discoHandler = disco.DiscoHandler()
-        self.discoHandler.setHandlerParent(self)
-        disco_d = defer.succeed(None)
+        self.identities = [disco.DiscoIdentity(u"component", u"generic", C.APP_NAME)]
+        # jid is set automatically on bind by Twisted for Client, but not for Component
+        self.jid = component_jid
+        if host is None:
+            try:
+                host = component_jid.host.split(u'.', 1)[1]
+            except IndexError:
+                raise ValueError(u"Can't guess host from jid, please specify a host")
+        # XXX: component.Component expect unicode jid, while Client expect jid.JID.
+        #      this is not consistent, so we use jid.JID for SatXMPP*
+        component.Component.__init__(self, host, port, component_jid.full(), password)
+        SatXMPPEntity.__init__(self, host_app, profile, max_retries)
 
-        if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
-            return
+    def _buildDependencies(self, current, plugins, required=True):
+        """build recursively dependencies needed for a plugin
 
-        def finish_connection(dummy):
-            self.roster.requestRoster()
-            self.presence.available()
-            self.conn_deferred.callback(None)
+        this method build list of plugin needed for a component and raises
+        errors if they are not available or not allowed for components
+        @param current(object): parent plugin to check
+            use entry_point for first call
+        @param plugins(list): list of validated plugins, will be filled by the method
+            give an empty list for first call
+        @param required(bool): True if plugin is mandatory
+            for recursive calls only, should not be modified by inital caller
+        @raise InternalError: one of the plugin is not handling components
+        @raise KeyError: one plugin should be present in self.host_app.plugins but it is not
+        """
+        if C.PLUG_MODE_COMPONENT not in current._info[u'modes']:
+            if not required:
+                return
+            else:
+                log.error(_(u"Plugin {current_name} if needed for {entry_name}, but it doesn't handle component mode").format(
+                    current_name = current._info[u'import_name'],
+                    entry_name = self.entry_plugin._info[u'import_name']
+                    ))
+                raise exceptions.InternalError(_(u"invalid plugin mode"))
 
-        disco_d.addCallback(finish_connection)
+        for import_name in current._info.get(C.PI_DEPENDENCIES, []):
+            # plugins are already loaded as dependencies
+            # so we know they are in self.host_app.plugins
+            dep = self.host_app.plugins[import_name]
+            self._checkDependencies(dep, plugins)
 
-    def initializationFailed(self, reason):
-        log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
-        self.conn_deferred.errback(reason.value)
-        try:
-            wokkel_client.XMPPClient.initializationFailed(self, reason)
-        except:
-            # we already chained an errback, no need to raise an exception
-            pass
+        for import_name in current._info.get(C.PI_RECOMMENDATIONS, []):
+            # here plugins are only recommendations,
+            # so they may not exist in self.host_app.plugins
+            try:
+                dep = self.host_app.plugins[import_name]
+            except KeyError:
+                continue
+            self._buildDependencies(dep, plugins, required = False)
+
+        if current not in plugins:
+            # current can be required for several plugins and so
+            # it can already be present in the list
+            plugins.append(current)
 
-    def isConnected(self):
-        return self.__connected
+    def _getPluginsList(self):
+        # XXX: for component we don't launch all plugins triggers
+        #      but only the ones from which there is a dependency
+        plugins = []
+        self._buildDependencies(self.entry_plugin, plugins)
+        return plugins
 
-    def connectionLost(self, connector, unused_reason):
-        try:
-            self.keep_alife.stop()
-        except AttributeError:
-            log.debug(_("No keep_alife"))
-        if self.__connected:
-            log.info(_("********** [%s] DISCONNECTED **********") % self.profile)
-            self.host_app.bridge.disconnected(self.profile)  # we send the signal to the clients
-            self.host_app.purgeClient(self.profile)  # and we remove references to this client
-        self.__connected = False
+    def entityConnected(self):
+        # we can now launch entry point
+        return self.entry_plugin.componentStart(self)
+
+    def addPostXmlCallbacks(self, post_xml_treatments):
+        if self.sendHistory:
+            post_xml_treatments.addCallback(self.messageAddToHistory)
 
 
 class SatMessageProtocol(xmppim.MessageProtocol):
@@ -432,8 +888,6 @@
 
     def isPresenceAuthorised(self, entity_jid):
         """Return True if entity is authorised to see our presence"""
-        import pudb
-        pudb.set_trace()
         try:
             item = self._jids[entity_jid.userhostJID()]
         except KeyError:
@@ -687,15 +1141,16 @@
         # it reject our features (resulting in e.g. no notification on PEP)
         return generic.VersionHandler.getDiscoInfo(self, requestor, target, None)
 
+
 class SatIdentityHandler(XMPPHandler):
-    """ Manage disco Identity of SàT. Currently, we use "client/pc/Salut à Toi", but as
-    SàT is multi-frontends and can be used on mobile devices, as a bot, with a web frontend,
-    etc, we should implement a way to dynamically update identities through the bridge """
+    """ Manage disco Identity of SàT.
+
+    """
     #TODO: dynamic identity update (see docstring). Note that a XMPP entity can have several identities
     implements(iwokkel.IDisco)
 
     def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
-        return [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)]
+        return self.parent.identities
 
     def getDiscoItems(self, requestor, target, nodeIdentifier=''):
         return []
--- a/src/memory/memory.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/memory/memory.py	Sun Feb 12 17:55:43 2017 +0100
@@ -453,12 +453,15 @@
 
         self.memory_data['Profile_default'] = profile
 
-    def asyncCreateProfile(self, name, password):
+    def createProfile(self, name, password, component=None):
         """Create a new profile
-        @param name (unicode): profile name
-        @param password (unicode): profile password
+
+        @param name(unicode): profile name
+        @param password(unicode): profile password
             Can be empty to disable password
+        @param component(None, unicode): set to entry point if this is a component
         @return: Deferred
+        @raise exceptions.NotFound: component is not a known plugin import name
         """
         if not name:
             raise ValueError(u"Empty profile name")
@@ -470,7 +473,16 @@
         if name in self._entities_cache:
             raise exceptions.ConflictError(u"A session for this profile exists")
 
-        d = self.params.asyncCreateProfile(name)
+        if component:
+            if not component in self.host.plugins:
+                raise exceptions.NotFound(_(u"Can't find component {component} entry point".format(
+                    component = component)))
+            # FIXME: PLUGIN_INFO is not currently accessible after import, but type shoul be tested here
+            # if self.host.plugins[component].PLUGIN_INFO[u"type"] != C.PLUG_TYPE_ENTRY_POINT:
+            #     raise ValueError(_(u"Plugin {component} is not an entry point !".format(
+            #         component = component)))
+
+        d = self.params.createProfile(name, component)
 
         def initPersonalKey(dummy):
             # be sure to call this after checking that the profile doesn't exist yet
@@ -495,6 +507,7 @@
 
     def asyncDeleteProfile(self, name, force=False):
         """Delete an existing profile
+
         @param name: Name of the profile
         @param force: force the deletion even if the profile is connected.
         To be used for direct calls only (not through the bridge).
@@ -510,6 +523,24 @@
         d.addCallback(cleanMemory)
         return d
 
+    def isComponent(self, profile_name):
+        """Tell if a profile is a component
+
+        @param profile_name(unicode): name of the profile
+        @return (bool): True if profile is a component
+        @raise exceptions.NotFound: profile doesn't exist
+        """
+        return self.storage.profileIsComponent(profile_name)
+
+    def getEntryPoint(self, profile_name):
+        """Get a component entry point
+
+        @param profile_name(unicode): name of the profile
+        @return (bool): True if profile is a component
+        @raise exceptions.NotFound: profile doesn't exist
+        """
+        return self.storage.getEntryPoint(profile_name)
+
     ## History ##
 
     def addToHistory(self, client, data):
--- a/src/memory/params.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/memory/params.py	Sun Feb 12 17:55:43 2017 +0100
@@ -166,10 +166,11 @@
         self.params = {}
         self.params_gen = {}
 
-    def asyncCreateProfile(self, profile):
+    def createProfile(self, profile, component):
         """Create a new profile
 
-        @param profile: name of the profile
+        @param profile(unicode): name of the profile
+        @param component(unicode): entry point if profile is a component
         @param callback: called when the profile actually exists in database and memory
         @return: a Deferred instance
         """
@@ -178,7 +179,7 @@
             return defer.fail(Failure(exceptions.ConflictError))
         if not self.host.trigger.point("ProfileCreation", profile):
             return defer.fail(Failure(exceptions.CancelError))
-        return self.storage.createProfile(profile)
+        return self.storage.createProfile(profile, component or None)
 
     def asyncDeleteProfile(self, profile, force=False):
         """Delete an existing profile
--- a/src/memory/sqlite.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/memory/sqlite.py	Sun Feb 12 17:55:43 2017 +0100
@@ -207,22 +207,37 @@
         except KeyError:
             raise exceptions.NotFound(u"the requested profile doesn't exists")
 
-    def createProfile(self, name):
+    def getEntryPoint(self, profile_name):
+        try:
+            return self.components[self.profiles[profile_name]]
+        except KeyError:
+            raise exceptions.NotFound(u"the requested profile doesn't exists or is not a component")
+
+    def createProfile(self, name, component=None):
         """Create a new profile
 
-        @param name: name of the profile
+        @param name(unicode): name of the profile
+        @param component(None, unicode): if not None, must point to a component entry point
         @return: deferred triggered once profile is actually created
         """
 
         def getProfileId(ignore):
             return self.dbpool.runQuery("SELECT (id) FROM profiles WHERE name = ?", (name, ))
 
+        def setComponent(profile_id):
+            id_ = profile_id[0][0]
+            d_comp = self.dbpool.runQuery("INSERT INTO components(profile_id, entry_point) VALUES (?, ?)", (id_, component))
+            d_comp.addCallback(lambda dummy: profile_id)
+            return d_comp
+
         def profile_created(profile_id):
-            _id = profile_id[0][0]
-            self.profiles[name] = _id  # we synchronise the cache
+            id_= profile_id[0][0]
+            self.profiles[name] = id_  # we synchronise the cache
 
         d = self.dbpool.runQuery("INSERT INTO profiles(name) VALUES (?)", (name, ))
         d.addCallback(getProfileId)
+        if component is not None:
+            d.addCallback(setComponent)
         d.addCallback(profile_created)
         return d
 
--- a/src/plugins/plugin_exp_command_export.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_exp_command_export.py	Sun Feb 12 17:55:43 2017 +0100
@@ -41,11 +41,11 @@
 class ExportCommandProtocol(protocol.ProcessProtocol):
     """ Try to register an account with prosody """
 
-    def __init__(self, parent, target, options, profile):
+    def __init__(self, parent, client, target, options):
         self.parent = parent
         self.target = target
         self.options = options
-        self.profile = profile
+        self.client = client
 
     def _clean(self, data):
         if not data:
@@ -58,10 +58,10 @@
         log.info("connectionMade :)")
 
     def outReceived(self, data):
-        self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile)
+        self.client.sendMessage(self.target, {'': self._clean(data)}, no_trigger=True)
 
     def errReceived(self, data):
-        self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile)
+        self.client.sendMessage(self.target, {'': self._clean(data)}, no_trigger=True)
 
     def processEnded(self, reason):
         log.info (u"process finished: %d" % (reason.value.exitCode,))
@@ -81,8 +81,9 @@
 
 class CommandExport(object):
     """Command export plugin: export a command to an entity"""
-    #XXX: This plugin can be potentially dangerous if we don't trust entities linked
-    #     this is specially true if we have other triggers.
+    # XXX: This plugin can be potentially dangerous if we don't trust entities linked
+    #      this is specially true if we have other triggers.
+    # FIXME: spawned should be a client attribute, not a class one
 
     def __init__(self, host):
         log.info(_("Plugin command export initialization"))
@@ -96,10 +97,10 @@
         @param entity: jid.JID attached to the process
         @param process: process to remove"""
         try:
-            processes_set = self.spawned[(entity, process.profile)]
+            processes_set = self.spawned[(entity, process.client.profile)]
             processes_set.discard(process)
             if not processes_set:
-                del(self.spawned[(entity, process.profile)])
+                del(self.spawned[(entity, process.client.profile)])
         except ValueError:
             pass
 
@@ -140,11 +141,7 @@
                         - pty: if set, launch in a pseudo terminal
                         - continue: continue normal MessageReceived handling
         """
-        profile = self.host.memory.getProfileName(profile_key)
-        if not profile:
-            log.warning(u"Unknown profile [%s]" % (profile,))
-            return
-
+        client = self.host.getClient(profile_key)
         for target in targets:
             try:
                 _jid = jid.JID(target)
@@ -154,7 +151,7 @@
             except (RuntimeError, jid.InvalidFormat, AttributeError):
                 log.info(u"invalid target ignored: %s" % (target,))
                 continue
-            process_prot = ExportCommandProtocol(self, _jid, options, profile)
-            self.spawned.setdefault((_jid, profile),set()).add(process_prot)
+            process_prot = ExportCommandProtocol(self, client, _jid, options)
+            self.spawned.setdefault((_jid, client.profile),set()).add(process_prot)
             reactor.spawnProcess(process_prot, command, args, usePTY = process_prot.boolOption('pty'))
 
--- a/src/plugins/plugin_exp_lang_detect.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_exp_lang_detect.py	Sun Feb 12 17:55:43 2017 +0100
@@ -65,7 +65,7 @@
         self.host = host
         host.memory.updateParams(PARAMS)
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger)
-        host.trigger.add("messageSend", self.MessageSendTrigger)
+        host.trigger.add("sendMessage", self.MessageSendTrigger)
 
     def addLanguage(self, mess_data):
         message = mess_data['message']
--- a/src/plugins/plugin_exp_parrot.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_exp_parrot.py	Sun Feb 12 17:55:43 2017 +0100
@@ -43,7 +43,7 @@
     """Parrot mode plugin: repeat messages from one entity or MUC room to another one"""
     # XXX: This plugin can be potentially dangerous if we don't trust entities linked
     #      this is specially true if we have other triggers.
-    #      messageSendTrigger avoid other triggers execution, it's deactivated to allow
+    #      sendMessageTrigger avoid other triggers execution, it's deactivated to allow
     #      /unparrot command in text commands plugin.
     # FIXME: potentially unsecure, specially with e2e encryption
 
@@ -51,13 +51,13 @@
         log.info(_("Plugin Parrot initialization"))
         self.host = host
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100)
-        #host.trigger.add("messageSend", self.messageSendTrigger, priority=100)
+        #host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100)
         try:
             self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
         except KeyError:
             log.info(_(u"Text commands not available"))
 
-    #def messageSendTrigger(self, client, mess_data, treatments):
+    #def sendMessageTrigger(self, client, mess_data, treatments):
     #    """ Deactivate other triggers if recipient is in parrot links """
     #    try:
     #        _links = client.parrot_links
@@ -103,7 +103,7 @@
 
             linked = _links[from_jid.userhostJID()]
 
-            self.host.messageSend(jid.JID(unicode(linked)), message, None, "auto", no_trigger=True, profile_key=profile)
+            client.sendMessage(jid.JID(unicode(linked)), message, None, "auto", no_trigger=True)
 
         return True
 
--- a/src/plugins/plugin_misc_account.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_account.py	Sun Feb 12 17:55:43 2017 +0100
@@ -242,7 +242,7 @@
         if profile.lower() in self.getConfig('reserved_list'):
             return defer.fail(Failure(exceptions.ConflictError))
 
-        d = self.host.memory.asyncCreateProfile(profile, password)
+        d = self.host.memory.createProfile(profile, password)
         d.addCallback(lambda dummy: self.profileCreated(password, jid_s, profile))
         return d
 
--- a/src/plugins/plugin_misc_file.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_file.py	Sun Feb 12 17:55:43 2017 +0100
@@ -219,7 +219,7 @@
             has_feature = yield self.host.hasFeature(namespace, peer_jid, profile)
             if has_feature:
                 log.info(u"{name} method will be used to send the file".format(name=method_name))
-                progress_id = yield defer.maybeDeferred(callback, peer_jid, filepath, filename, file_desc, profile)
+                progress_id = yield callback(peer_jid, filepath, filename, file_desc, profile)
                 defer.returnValue({'progress': progress_id})
         msg = u"Can't find any method to send file to {jid}".format(jid=peer_jid.full())
         log.warning(msg)
--- a/src/plugins/plugin_misc_groupblog.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_groupblog.py	Sun Feb 12 17:55:43 2017 +0100
@@ -65,14 +65,13 @@
 
     ## plugin management methods ##
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return GroupBlog_handler()
 
     @defer.inlineCallbacks
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
+    def profileConnected(self, client):
         try:
-            yield self.host.checkFeatures((NS_PUBSUB_GROUPBLOG,), profile=profile)
+            yield self.host.checkFeatures((NS_PUBSUB_GROUPBLOG,), profile=client.profile)
         except exceptions.FeatureNotFound:
             client.server_groupblog_available = False
             log.warning(_(u"Server is not able to manage item-access pubsub, we can't use group blog"))
--- a/src/plugins/plugin_misc_ip.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_ip.py	Sun Feb 12 17:55:43 2017 +0100
@@ -103,7 +103,7 @@
         self._external_ip_cache = None
         self._local_ip_cache = None
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return IPPlugin_handler()
 
     def refreshIP(self):
--- a/src/plugins/plugin_misc_maildir.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_maildir.py	Sun Feb 12 17:55:43 2017 +0100
@@ -45,7 +45,7 @@
 MAILDIR_PATH = "Maildir"
 CATEGORY = D_("Mail Server")
 NAME = D_('Block "normal" messages propagation')
-# FIXME: (very) old and (very) experimental code, need a big cleaning/review
+# FIXME: (very) old and (very) experimental code, need a big cleaning/review or to be deprecated
 
 
 class MaildirError(Exception):
@@ -82,8 +82,9 @@
         #the triggers
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
 
-    def profileConnected(self, profile):
-        """Called on profile connection, create profile data"""
+    def profileConnected(self, client):
+        """Called on client connection, create profile data"""
+        profile = client.profile
         self.data[profile] = PersistentBinaryDict("plugin_maildir", profile)
         self.__mailboxes[profile] = {}
 
@@ -93,8 +94,9 @@
                 self.data[profile]["INBOX"] = {"cur_idx": 0}
         self.data[profile].load().addCallback(dataLoaded)
 
-    def profileDisconnected(self, profile):
+    def profileDisconnected(self, client):
         """Called on profile disconnection, free profile's resources"""
+        profile = client.profile
         del self.__mailboxes[profile]
         del self.data[profile]
 
--- a/src/plugins/plugin_misc_room_game.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_room_game.py	Sun Feb 12 17:55:43 2017 +0100
@@ -683,7 +683,7 @@
         """
         return self._sendElements(to_jid, [(elem, attrs, content)], profile)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return RoomGameHandler(self)
 
 
--- a/src/plugins/plugin_misc_smtp.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_smtp.py	Sun Feb 12 17:55:43 2017 +0100
@@ -87,7 +87,7 @@
         """handle end of message"""
         mail = Parser().parsestr("\n".join(self.message))
         try:
-            self.host._messageSend(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'),  # TODO: manage other charsets
+            self.host._sendMessage(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'),  # TODO: manage other charsets
                                   subject=mail['subject'].decode('utf-8', 'replace'), mess_type='normal', profile_key=self.profile)
         except:
             exc_type, exc_value, exc_traceback = sys.exc_info()
--- a/src/plugins/plugin_misc_text_commands.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_text_commands.py	Sun Feb 12 17:55:43 2017 +0100
@@ -60,7 +60,7 @@
         log.info(_("Text commands initialization"))
         self.host = host
         # this is internal command, so we set high priority
-        host.trigger.add("messageSend", self.messageSendTrigger, priority=1000000)
+        host.trigger.add("sendMessage", self.sendMessageTrigger, priority=1000000)
         self._commands = {}
         self._whois = []
         self.registerTextCommands(self)
@@ -168,12 +168,12 @@
         self._whois.append((priority, callback))
         self._whois.sort(key=lambda item: item[0], reverse=True)
 
-    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """Install SendMessage command hook """
-        pre_xml_treatments.addCallback(self._messageSendCmdHook, client)
+        pre_xml_treatments.addCallback(self._sendMessageCmdHook, client)
         return True
 
-    def _messageSendCmdHook(self, mess_data, client):
+    def _sendMessageCmdHook(self, mess_data, client):
         """ Check text commands in message, and react consequently
 
         msg starting with / are potential command. If a command is found, it is executed, else and help message is sent
@@ -181,7 +181,7 @@
         commands can abord message sending (if they return anything evaluating to False), or continue it (if they return True), eventually after modifying the message
         an "unparsed" key is added to message, containing part of the message not yet parsed
         commands can be deferred or not
-        @param mess_data(dict): data comming from messageSend trigger
+        @param mess_data(dict): data comming from sendMessage trigger
         @param profile: %(doc_profile)s
         """
         try:
@@ -254,7 +254,7 @@
     def _contextValid(self, mess_data, cmd_data):
         """Tell if a command can be used in the given context
 
-        @param mess_data(dict): message data as given in messageSend trigger
+        @param mess_data(dict): message data as given in sendMessage trigger
         @param cmd_data(dict): command data as returned by self._parseDocString
         @return (bool): True if command can be used in this context
         """
@@ -290,7 +290,7 @@
         mess_data["type"] = C.MESS_TYPE_INFO
         mess_data["message"] = {'': message}
         mess_data["extra"]["info_type"] = info_type
-        self.host.messageSendToBridge(mess_data, client)
+        self.host.sendMessageToBridge(mess_data, client)
 
     def cmd_whois(self, client, mess_data):
         """show informations on entity
--- a/src/plugins/plugin_misc_upload.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_upload.py	Sun Feb 12 17:55:43 2017 +0100
@@ -114,7 +114,7 @@
                 continue # no entity managing this extension found
 
             log.info(u"{name} method will be used to upload the file".format(name=method_name))
-            progress_id_d, download_d = yield defer.maybeDeferred(upload_cb, filepath, filename, upload_jid, options, client.profile)
+            progress_id_d, download_d = yield upload_cb(filepath, filename, upload_jid, options, client.profile)
             progress_id = yield progress_id_d
             defer.returnValue((progress_id, download_d))
 
--- a/src/plugins/plugin_misc_welcome.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_misc_welcome.py	Sun Feb 12 17:55:43 2017 +0100
@@ -70,10 +70,10 @@
         self.host = host
         host.memory.updateParams(PARAMS)
 
-    def profileConnected(self, profile):
+    def profileConnected(self, client):
         # XXX: if you wan to try first_start again, you'll have to remove manually
         #      the welcome value from your profile params in sat.db
-        welcome = self.host.memory.params.getParamA(WELCOME_PARAM_NAME, WELCOME_PARAM_CATEGORY, use_default=False, profile_key=profile)
+        welcome = self.host.memory.params.getParamA(WELCOME_PARAM_NAME, WELCOME_PARAM_CATEGORY, use_default=False, profile_key=client.profile)
         if welcome is None:
             first_start = True
             welcome = True
@@ -82,10 +82,10 @@
 
         if welcome:
             xmlui = xml_tools.note(WELCOME_MSG, WELCOME_MSG_TITLE)
-            self.host.actionNew({'xmlui': xmlui.toXml()}, profile=profile)
-            self.host.memory.setParam(WELCOME_PARAM_NAME, C.BOOL_FALSE, WELCOME_PARAM_CATEGORY, profile_key=profile)
+            self.host.actionNew({'xmlui': xmlui.toXml()}, profile=client.profile)
+            self.host.memory.setParam(WELCOME_PARAM_NAME, C.BOOL_FALSE, WELCOME_PARAM_CATEGORY, profile_key=client.profile)
 
-        self.host.trigger.point("WELCOME", first_start, welcome, profile)
+        self.host.trigger.point("WELCOME", first_start, welcome, client.profile)
 
 
 
--- a/src/plugins/plugin_sec_otr.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_sec_otr.py	Sun Feb 12 17:55:43 2017 +0100
@@ -81,7 +81,7 @@
     def inject(self, msg_str, appdata=None):
         """Inject encrypted data in the stream
 
-        if appdata is not None, we are sending a message in sendMessageFinishTrigger
+        if appdata is not None, we are sending a message in sendMessageDataTrigger
         stanza will be injected directly if appdata is None, else we just update the element
         and follow normal workflow
         @param msg_str(str): encrypted message body
@@ -103,7 +103,7 @@
                          'extra': {},
                          'timestamp': time.time(),
                         }
-            self.host.generateMessageXML(mess_data)
+            client.generateMessageXML(mess_data)
             client.send(mess_data['xml'])
         else:
             message_elt = appdata[u'xml']
@@ -234,8 +234,8 @@
         self._p_hints = host.plugins[u'XEP-0334']
         self._p_carbons = host.plugins[u'XEP-0280']
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
-        host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
-        host.trigger.add("sendMessageFinish", self._sendMessageFinishTrigger)
+        host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
+        host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
         host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)  # FIXME: must be removed, must be done on per-message basis
         host.bridge.addSignal("otrState", ".plugin", signature='sss')  # args: state, destinee_jid, profile
         host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE)
@@ -255,26 +255,24 @@
         self.skipped_profiles.add(profile)
 
     @defer.inlineCallbacks
-    def profileConnected(self, profile):
-        if profile in self.skipped_profiles:
+    def profileConnected(self, client):
+        if client.profile in self.skipped_profiles:
             return
-        client = self.host.getClient(profile)
         ctxMng = client._otr_context_manager = ContextManager(self.host, client)
-        client._otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
+        client._otr_data = persistent.PersistentBinaryDict(NS_OTR, client.profile)
         yield client._otr_data.load()
         encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
         if encrypted_priv_key is not None:
-            priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile)
+            priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, client.profile)
             ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
         else:
             ctxMng.account.privkey = None
         ctxMng.account.loadTrusts()
 
-    def profileDisconnected(self, profile):
-        if profile in self.skipped_profiles:
-            self.skipped_profiles.remove(profile)
+    def profileDisconnected(self, client):
+        if client.profile in self.skipped_profiles:
+            self.skipped_profiles.remove(client.profile)
             return
-        client = self.host.getClient(profile)
         for context in client._otr_context_manager.contexts.values():
             context.disconnect()
         del client._otr_context_manager
@@ -454,7 +452,7 @@
                     jid = from_jid.full()))
 
                 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
-                client.feedback(from_jid.full(), feedback)
+                client.feedback(from_jid, feedback)
         except StopIteration:
             return data
         else:
@@ -505,7 +503,7 @@
             post_treat.addCallback(self._receivedTreatment, client)
         return True
 
-    def _sendMessageFinishTrigger(self, client, mess_data):
+    def _sendMessageDataTrigger(self, client, mess_data):
         if not 'OTR' in mess_data:
             return
         otrctx = mess_data['OTR']
@@ -532,9 +530,11 @@
         else:
             feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. "
                           u"Either close your own side, or refresh the session.")
-            client.feedback(to_jid.full(), feedback)
+            log.warning(_(u"Message discarded because closed encryption channel"))
+            client.feedback(to_jid, feedback)
+            raise failure.Failure(exceptions.CancelError(u'Cancelled by OTR plugin'))
 
-    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         if mess_data['type'] == 'groupchat':
             return True
         if client.profile in self.skipped_profiles:  # FIXME: should not be done on a per-profile basis
@@ -546,7 +546,7 @@
         if otrctx.state != potr.context.STATE_PLAINTEXT:
             self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_COPY)
             self._p_hints.addHint(mess_data, self._p_hints.HINT_NO_PERMANENT_STORE)
-            mess_data['OTR'] = otrctx  # this indicate that encryption is needed in sendMessageFinish trigger
+            mess_data['OTR'] = otrctx  # this indicate that encryption is needed in sendMessageData trigger
             if not mess_data['to'].resource:  # if not resource was given, we force it here
                 mess_data['to'] = to_jid
         return True
--- a/src/plugins/plugin_xep_0020.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0020.py	Sun Feb 12 17:55:43 2017 +0100
@@ -50,7 +50,7 @@
     def __init__(self, host):
         log.info(_("Plugin XEP_0020 initialization"))
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0020_handler()
 
     def getFeatureElt(self, elt):
--- a/src/plugins/plugin_xep_0033.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0033.py	Sun Feb 12 17:55:43 2017 +0100
@@ -72,10 +72,10 @@
         log.info(_("Extended Stanza Addressing plugin initialization"))
         self.host = host
         self.internal_data = {}
-        host.trigger.add("messageSend", self.messageSendTrigger, trigger.TriggerManager.MIN_PRIORITY)
+        host.trigger.add("sendMessage", self.sendMessageTrigger, trigger.TriggerManager.MIN_PRIORITY)
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
 
-    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """Process the XEP-0033 related data to be sent"""
         profile = client.profile
 
@@ -124,9 +124,9 @@
             client = self.host.profiles[profile]
             d = defer.Deferred()
             if not skip_send:
-                d.addCallback(client.sendMessage)
+                d.addCallback(client.sendMessageData)
             d.addCallback(self.host.messageAddToHistory, client)
-            d.addCallback(self.host.messageSendToBridge, client)
+            d.addCallback(self.host.sendMessageToBridge, client)
             d.addErrback(lambda failure: failure.trap(exceptions.CancelError))
             return d.callback(mess_data)
 
@@ -181,8 +181,8 @@
             post_treat.addCallback(post_treat_addr, addresses.children)
         return True
 
-    def getHandler(self, profile):
-        return XEP_0033_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0033_handler(self, client.profile)
 
 
 class XEP_0033_handler(XMPPHandler):
--- a/src/plugins/plugin_xep_0045.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0045.py	Sun Feb 12 17:55:43 2017 +0100
@@ -104,11 +104,10 @@
         host.trigger.add("presence_available", self.presenceTrigger)
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000)
 
-    def profileConnected(self, profile):
+    def profileConnected(self, client):
         def assign_service(service):
-            client = self.host.getClient(profile)
             client.muc_service = service
-        return self.getMUCService(profile=profile).addCallback(assign_service)
+        return self.getMUCService(profile=client.profile).addCallback(assign_service)
 
     def MessageReceivedTrigger(self, client, message_elt, post_treat):
         if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
@@ -477,9 +476,8 @@
         self.checkRoomJoined(client, room_jid)
         return client._muc_client.subject(room_jid, subject)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         # create a MUC client and associate it with profile' session
-        client = self.host.getClient(profile)
         muc_client = client._muc_client = SatMUCClient(self)
         return muc_client
 
@@ -886,7 +884,7 @@
                     "timestamp": time.time(),
                 }
                 self.host.messageAddToHistory(mess_data, self.parent)
-                self.host.messageSendToBridge(mess_data, self.parent)
+                self.host.sendMessageToBridge(mess_data, self.parent)
 
 
     def userLeftRoom(self, room, user):
@@ -925,7 +923,7 @@
                 "timestamp": time.time(),
             }
             self.host.messageAddToHistory(mess_data, self.parent)
-            self.host.messageSendToBridge(mess_data, self.parent)
+            self.host.sendMessageToBridge(mess_data, self.parent)
 
     def userChangedNick(self, room, user, new_nick):
         self.host.bridge.mucRoomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile)
--- a/src/plugins/plugin_xep_0047.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0047.py	Sun Feb 12 17:55:43 2017 +0100
@@ -69,11 +69,10 @@
         log.info(_("In-Band Bytestreams plugin initialization"))
         self.host = host
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0047_handler(self)
 
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
+    def profileConnected(self, client):
         client.xep_0047_current_stream = {}  # key: stream_id, value: data(dict)
 
     def _timeOut(self, sid, client):
--- a/src/plugins/plugin_xep_0048.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0048.py	Sun Feb 12 17:55:43 2017 +0100
@@ -73,14 +73,13 @@
             log.info(_("Text commands not available"))
 
     @defer.inlineCallbacks
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
-        local = client.bookmarks_local = PersistentBinaryDict(NS_BOOKMARKS, profile)
+    def profileConnected(self, client):
+        local = client.bookmarks_local = PersistentBinaryDict(NS_BOOKMARKS, client.profile)
         yield local.load()
         if not local:
             local[XEP_0048.MUC_TYPE] = dict()
             local[XEP_0048.URL_TYPE] = dict()
-        private = yield self._getServerBookmarks('private', profile)
+        private = yield self._getServerBookmarks('private', client.profile)
         pubsub = client.bookmarks_pubsub = None
 
         for bookmarks in (local, private, pubsub):
--- a/src/plugins/plugin_xep_0050.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0050.py	Sun Feb 12 17:55:43 2017 +0100
@@ -221,15 +221,15 @@
         self.__requesting_id = host.registerCallback(self._requestingEntity, with_data=True)
         host.importMenu((D_("Service"), D_("Commands")), self._commandsMenu, security_limit=2, help_string=D_("Execute ad-hoc commands"))
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0050_handler(self)
 
-    def profileConnected(self, profile):
-        self.addAdHocCommand(self._statusCallback, _("Status"), profile_key=profile)
+    def profileConnected(self, client):
+        self.addAdHocCommand(self._statusCallback, _("Status"), profile_key=client.profile)
 
-    def profileDisconnected(self, profile):
+    def profileDisconnected(self, client):
         try:
-            del self.answering[profile]
+            del self.answering[client.profile]
         except KeyError:
             pass
 
--- a/src/plugins/plugin_xep_0054.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0054.py	Sun Feb 12 17:55:43 2017 +0100
@@ -88,7 +88,7 @@
         host.memory.setSignalOnUpdate(u"avatar")
         host.memory.setSignalOnUpdate(u"nick")
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0054_handler(self)
 
     def isRoom(self, client, entity_jid):
@@ -128,11 +128,10 @@
         return True
 
     @defer.inlineCallbacks
-    def profileConnecting(self, profile):
-        client = self.host.getClient(profile)
-        client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, profile)
+    def profileConnecting(self, client):
+        client._cache_0054 = persistent.PersistentBinaryDict(NS_VCARD, client.profile)
         yield client._cache_0054.load()
-        self._fillCachedValues(profile)
+        self._fillCachedValues(client.profile)
 
     def _fillCachedValues(self, profile):
         #FIXME: this may need to be reworked
--- a/src/plugins/plugin_xep_0059.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0059.py	Sun Feb 12 17:55:43 2017 +0100
@@ -47,7 +47,7 @@
     def __init__(self, host):
         log.info(_("Result Set Management plugin initialization"))
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0059_handler()
 
 
--- a/src/plugins/plugin_xep_0060.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0060.py	Sun Feb 12 17:55:43 2017 +0100
@@ -94,15 +94,13 @@
         host.bridge.addMethod("psGetFromManyRTResult", ".plugin", in_sign='ss', out_sign='(ua(sssasa{ss}))', method=self._getFromManyRTResult, async=True)
         host.bridge.addSignal("psEvent", ".plugin", signature='ssssa{ss}s')  # args: category, service(jid), node, type (C.PS_ITEMS, C.PS_DELETE), data, profile
 
-    def getHandler(self, profile):
-        client = self.host.getClient(profile)
+    def getHandler(self, client):
         client.pubsub_client = SatPubSubClient(self.host, self)
         return client.pubsub_client
 
     @defer.inlineCallbacks
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
-        pubsub_services = yield self.host.findServiceEntities("pubsub", "service", profile=profile)
+    def profileConnected(self, client):
+        pubsub_services = yield self.host.findServiceEntities("pubsub", "service", profile=client.profile)
         if pubsub_services:
             # we use one of the found services as our default pubsub service
             client.pubsub_service = pubsub_services.pop()
--- a/src/plugins/plugin_xep_0065.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0065.py	Sun Feb 12 17:55:43 2017 +0100
@@ -715,11 +715,10 @@
         # XXX: params are not used for now, but they may be used in the futur to force proxy/IP
         # host.memory.updateParams(PARAMS)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0065_handler(self)
 
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
+    def profileConnected(self, client):
         client.xep_0065_sid_session = {}  # key: stream_id, value: session_data(dict)
         client._s5b_sessions = {}
 
--- a/src/plugins/plugin_xep_0070.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0070.py	Sun Feb 12 17:55:43 2017 +0100
@@ -64,8 +64,8 @@
         self.host = host
         self._dictRequest = dict()
 
-    def getHandler(self, profile):
-        return XEP_0070_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0070_handler(self, client.profile)
 
     def onHttpAuthRequestIQ(self, iq_elt, client):
         """This method is called on confirmation request received (XEP-0070 #4.5)
--- a/src/plugins/plugin_xep_0071.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0071.py	Sun Feb 12 17:55:43 2017 +0100
@@ -81,9 +81,9 @@
         self._s = self.host.plugins["TEXT-SYNTAXES"]
         self._s.addSyntax(self.SYNTAX_XHTML_IM, lambda xhtml: xhtml, self.XHTML2XHTML_IM, [self._s.OPT_HIDDEN])
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
-        host.trigger.add("messageSend", self.messageSendTrigger)
+        host.trigger.add("sendMessage", self.sendMessageTrigger)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0071_handler(self)
 
     def _messagePostTreat(self, data, message_elt, body_elts, client):
@@ -129,10 +129,10 @@
             d.addCallback(self._fill_body_text, data, lang)
             defers.append(d)
 
-    def _messageSendAddRich(self, data, client):
+    def _sendMessageAddRich(self, data, client):
         """ Construct XHTML-IM node and add it XML element
 
-        @param data: message data as sended by messageSend callback
+        @param data: message data as sended by sendMessage callback
         """
         # at this point, either ['extra']['rich'] or ['extra']['xhtml'] exists
         # but both can't exist at the same time
@@ -183,7 +183,7 @@
             post_treat.addCallback(self._messagePostTreat, message, body_elts, client)
         return True
 
-    def messageSendTrigger(self, client, data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, data, pre_xml_treatments, post_xml_treatments):
         """ Check presence of rich text in extra """
         rich = {}
         xhtml = {}
@@ -199,7 +199,7 @@
                 data['rich'] = rich
             else:
                 data['xhtml'] = xhtml
-            post_xml_treatments.addCallback(self._messageSendAddRich, client)
+            post_xml_treatments.addCallback(self._sendMessageAddRich, client)
         return True
 
     def _purgeStyle(self, styles_raw):
--- a/src/plugins/plugin_xep_0085.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0085.py	Sun Feb 12 17:55:43 2017 +0100
@@ -103,7 +103,7 @@
 
         # triggers from core
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
-        host.trigger.add("messageSend", self.messageSendTrigger)
+        host.trigger.add("sendMessage", self.sendMessageTrigger)
         host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger)
 
         # args: to_s (jid as string), profile
@@ -113,11 +113,12 @@
         # args: from (jid as string), state in CHAT_STATES, profile
         host.bridge.addSignal("chatStateReceived", ".plugin", signature='sss')
 
-    def getHandler(self, profile):
-        return XEP_0085_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0085_handler(self, client.profile)
 
-    def profileDisconnected(self, profile):
+    def profileDisconnected(self, client):
         """Eventually send a 'gone' state to all one2one contacts."""
+        profile = client.profile
         if profile not in self.map:
             return
         for to_jid in self.map[profile]:
@@ -198,7 +199,7 @@
             break
         return True
 
-    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """
         Eventually add the chat state to the message and initiate
         the state machine when sending an "active" state.
@@ -371,7 +372,7 @@
                     'subject': {},
                     'extra': {},
                     }
-                self.host.generateMessageXML(mess_data)
+                client.generateMessageXML(mess_data)
                 mess_data['xml'].addElement(state, NS_CHAT_STATES)
                 client.send(mess_data['xml'])
 
--- a/src/plugins/plugin_xep_0095.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0095.py	Sun Feb 12 17:55:43 2017 +0100
@@ -55,7 +55,7 @@
         self.host = host
         self.si_profiles = {}  # key: SI profile, value: callback
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0095_handler(self)
 
     def registerSIProfile(self, si_profile, callback):
--- a/src/plugins/plugin_xep_0115.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0115.py	Sun Feb 12 17:55:43 2017 +0100
@@ -58,8 +58,8 @@
         host.trigger.add("Disco handled", self._checkHash)
         host.trigger.add("Presence send", self._presenceTrigger)
 
-    def getHandler(self, profile):
-        return XEP_0115_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0115_handler(self, client.profile)
 
     def _checkHash(self, disco_d, profile):
         client = self.host.getClient(profile)
--- a/src/plugins/plugin_xep_0166.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0166.py	Sun Feb 12 17:55:43 2017 +0100
@@ -97,11 +97,10 @@
                                   XEP_0166.TRANSPORT_STREAMING: [],
                                 }
 
-    def profileConnected(self, profile):
-        client = self.host.getClient(profile)
+    def profileConnected(self, client):
         client.jingle_sessions = {}  # key = sid, value = session_data
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0166_handler(self)
 
     def _delSession(self, client, sid):
--- a/src/plugins/plugin_xep_0184.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0184.py	Sun Feb 12 17:55:43 2017 +0100
@@ -92,13 +92,13 @@
         # parameter value is retrieved before each use
         host.memory.updateParams(self.params)
 
-        host.trigger.add("messageSend", self.messageSendTrigger)
+        host.trigger.add("sendMessage", self.sendMessageTrigger)
         host.bridge.addSignal("messageState", ".plugin", signature='sss')  # message_uid, status, profile
 
-    def getHandler(self, profile):
-        return XEP_0184_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0184_handler(self, client.profile)
 
-    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """Install SendMessage command hook """
         def treatment(mess_data):
             message = mess_data['xml']
--- a/src/plugins/plugin_xep_0203.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0203.py	Sun Feb 12 17:55:43 2017 +0100
@@ -49,8 +49,8 @@
         log.info(_("Delayed Delivery plugin initialization"))
         self.host = host
 
-    def getHandler(self, profile):
-        return XEP_0203_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0203_handler(self, client.profile)
 
     def delay(self, stamp, sender=None, desc='', parent=None):
         """Build a delay element, eventually append it to the given parent element.
--- a/src/plugins/plugin_xep_0231.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0231.py	Sun Feb 12 17:55:43 2017 +0100
@@ -77,7 +77,7 @@
 
         return file_path
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0231_handler()
 
     def _dataCb(self, iq_elt, client, img_elt, cid):
--- a/src/plugins/plugin_xep_0234.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0234.py	Sun Feb 12 17:55:43 2017 +0100
@@ -63,7 +63,7 @@
         self._hash = self.host.plugins["XEP-0300"]
         host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='sssss', out_sign='', method=self._fileJingleSend)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0234_handler()
 
     def _getProgressId(self, session, content_name):
--- a/src/plugins/plugin_xep_0249.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0249.py	Sun Feb 12 17:55:43 2017 +0100
@@ -88,7 +88,7 @@
         except KeyError:
             log.info(_("Text commands not available"))
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0249_handler(self)
 
     def _invite(self, guest_jid_s, room_jid_s, options, profile_key):
--- a/src/plugins/plugin_xep_0260.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0260.py	Sun Feb 12 17:55:43 2017 +0100
@@ -69,7 +69,7 @@
             self._jingle_ibb = None
         self._j.registerTransport(NS_JINGLE_S5B, self._j.TRANSPORT_STREAMING, self, 100)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0260_handler()
 
     def _parseCandidates(self, transport_elt):
--- a/src/plugins/plugin_xep_0261.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0261.py	Sun Feb 12 17:55:43 2017 +0100
@@ -55,7 +55,7 @@
         self._ibb = host.plugins["XEP-0047"] # and in-band bytestream
         self._j.registerTransport(NS_JINGLE_IBB, self._j.TRANSPORT_STREAMING, self, -10000) # must be the lowest priority
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0261_handler()
 
     def jingleSessionInit(self, session, content_name, profile):
--- a/src/plugins/plugin_xep_0280.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0280.py	Sun Feb 12 17:55:43 2017 +0100
@@ -76,7 +76,7 @@
         host.memory.updateParams(self.params)
         host.trigger.add("MessageReceived", self.messageReceivedTrigger, priority=1000)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0280_handler()
 
     def setPrivate(self, message_elt):
@@ -92,15 +92,14 @@
         message_elt.addElement((NS_CARBONS, u'private'))
 
     @defer.inlineCallbacks
-    def profileConnected(self, profile):
+    def profileConnected(self, client):
         """activate message carbons on connection if possible and activated in config"""
-        client = self.host.getClient(profile)
-        activate = self.host.memory.getParamA(PARAM_NAME, PARAM_CATEGORY, profile_key=profile)
+        activate = self.host.memory.getParamA(PARAM_NAME, PARAM_CATEGORY, profile_key=client.profile)
         if not activate:
             log.info(_(u"Not activating message carbons as requested in params"))
             return
         try:
-            yield self.host.checkFeatures((NS_CARBONS,), profile=profile)
+            yield self.host.checkFeatures((NS_CARBONS,), profile=client.profile)
         except exceptions.FeatureNotFound:
             log.warning(_(u"server doesn't handle message carbons"))
         else:
@@ -149,7 +148,7 @@
             if not mess_data['message'] and not mess_data['subject']:
                 return False
             self.host.messageAddToHistory(mess_data, client)
-            self.host.messageSendToBridge(mess_data, client)
+            self.host.sendMessageToBridge(mess_data, client)
         else:
             log.warning(u"invalid message carbons received:\n{xml}".format(
                 xml = message_elt.toXml()))
--- a/src/plugins/plugin_xep_0297.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0297.py	Sun Feb 12 17:55:43 2017 +0100
@@ -52,8 +52,8 @@
         log.info(_("Stanza Forwarding plugin initialization"))
         self.host = host
 
-    def getHandler(self, profile):
-        return XEP_0297_handler(self, profile)
+    def getHandler(self, client):
+        return XEP_0297_handler(self, client.profile)
 
     @classmethod
     def updateUri(cls, element, uri):
@@ -80,7 +80,7 @@
         @param profile_key (unicode): %(doc_profile_key)s
         @return: a Deferred when the message has been sent
         """
-        # FIXME: this method is not used and doesn't use mess_data which should be used for client.sendMessage
+        # FIXME: this method is not used and doesn't use mess_data which should be used for client.sendMessageData
         #        should it be deprecated? A method constructing the element without sending it seems more natural
         log.warning(u"THIS METHOD IS DEPRECATED") # FIXME: we use this warning until we check the method
         msg = domish.Element((None, 'message'))
@@ -102,7 +102,7 @@
         msg.addChild(forwarded_elt)
 
         client = self.host.getClient(profile_key)
-        return client.sendMessage({u'xml': msg})
+        return client.sendMessageData({u'xml': msg})
 
 
 class XEP_0297_handler(XMPPHandler):
--- a/src/plugins/plugin_xep_0300.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0300.py	Sun Feb 12 17:55:43 2017 +0100
@@ -58,7 +58,7 @@
     def __init__(self, host):
         log.info(_("plugin Hashes initialization"))
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0300_handler()
 
     def getHasher(self, algo):
--- a/src/plugins/plugin_xep_0313.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0313.py	Sun Feb 12 17:55:43 2017 +0100
@@ -55,8 +55,7 @@
         log.info(_("Message Archive Management plugin initialization"))
         self.host = host
 
-    def getHandler(self, profile):
-        client = self.host.getClient(profile)
+    def getHandler(self, client):
         mam_client = client._mam = SatMAMClient()
         return mam_client
 
--- a/src/plugins/plugin_xep_0334.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0334.py	Sun Feb 12 17:55:43 2017 +0100
@@ -62,10 +62,10 @@
     def __init__(self, host):
         log.info(_("Message Processing Hints plugin initialization"))
         self.host = host
-        host.trigger.add("messageSend", self.messageSendTrigger)
+        host.trigger.add("sendMessage", self.sendMessageTrigger)
         host.trigger.add("MessageReceived", self.messageReceivedTrigger, priority=-1000)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0334_handler()
 
     def addHint(self, mess_data, hint):
@@ -84,7 +84,7 @@
                 mess_data[u'xml'].addElement((NS_HINTS, hint))
         return mess_data
 
-    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+    def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """Add the hints element to the message to be sent"""
         if u'hints' in mess_data[u'extra']:
             for hint in data_format.dict2iter(u'hints', mess_data[u'extra'], pop=True):
--- a/src/plugins/plugin_xep_0363.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/plugins/plugin_xep_0363.py	Sun Feb 12 17:55:43 2017 +0100
@@ -94,7 +94,7 @@
         host.bridge.addMethod("fileHTTPUploadGetSlot", ".plugin", in_sign='sisss', out_sign='(ss)', method=self._getSlot, async=True)
         host.plugins['UPLOAD'].register(u"HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload)
 
-    def getHandler(self, profile):
+    def getHandler(self, client):
         return XEP_0363_handler()
 
     @defer.inlineCallbacks