changeset 1955:633b5c21aefd

backend, frontend: messages refactoring (huge commit, not finished): /!\ database schema has been modified, do a backup before updating message have been refactored, here are the main changes: - languages are now handled - all messages have an uid (internal to SàT) - message updating is anticipated - subject is now first class - new naming scheme is used newMessage => messageNew, getHistory => historyGet, sendMessage => messageSend - minimal compatibility refactoring in quick_frontend/Primitivus, better refactoring should follow - threads handling - delayed messages are saved into history - info messages may also be saved in history (e.g. to keep track of people joining/leaving a room) - duplicate messages should be avoided - historyGet return messages in right order, no need to sort again - plugins have been updated to follow new features, some of them need to be reworked (e.g. OTR) - XEP-0203 (Delayed Delivery) is now fully handled in core, the plugin just handle disco and creation of a delay element - /!\ jp and Libervia are currently broken, as some features of Primitivus It has been put in one huge commit to avoid breaking messaging between changes. This is the main part of message refactoring, other commits will follow to take profit of the new features/behaviour.
author Goffi <goffi@goffi.org>
date Tue, 24 May 2016 22:11:04 +0200
parents ccfe45302a5c
children ca5a883f8abe
files frontends/src/bridge/DBus.py frontends/src/primitivus/chat.py frontends/src/primitivus/primitivus frontends/src/quick_frontend/quick_app.py frontends/src/quick_frontend/quick_chat.py frontends/src/quick_frontend/quick_contact_list.py frontends/src/quick_frontend/quick_widgets.py src/bridge/DBus.py src/bridge/bridge_constructor/bridge_template.ini src/core/sat_main.py src/core/xmpp.py src/memory/memory.py src/memory/sqlite.py src/plugins/plugin_exp_command_export.py src/plugins/plugin_exp_parrot.py src/plugins/plugin_misc_imap.py src/plugins/plugin_misc_maildir.py src/plugins/plugin_misc_smtp.py src/plugins/plugin_misc_text_commands.py src/plugins/plugin_sec_otr.py src/plugins/plugin_xep_0033.py src/plugins/plugin_xep_0071.py src/plugins/plugin_xep_0085.py src/plugins/plugin_xep_0203.py src/plugins/plugin_xep_0334.py src/test/helpers.py src/test/test_core_xmpp.py src/test/test_plugin_xep_0033.py src/test/test_plugin_xep_0085.py src/test/test_plugin_xep_0334.py
diffstat 30 files changed, 778 insertions(+), 447 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/DBus.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/bridge/DBus.py	Tue May 24 22:11:04 2016 +0200
@@ -336,15 +336,6 @@
             error_handler = lambda err:errback(dbus_to_bridge_exception(err))
         return self.db_core_iface.getFeatures(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
 
-    def getHistory(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", 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.getHistory(from_jid, to_jid, limit, between, search, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
-
     def getMainResource(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None):
         if callback is None:
             error_handler = None
@@ -517,6 +508,15 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.getWaitingSub(profile_key, **kwargs)
 
+    def historyGet(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", 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.historyGet(from_jid, to_jid, limit, between, search, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
+
     def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None):
         if callback is None:
             error_handler = None
@@ -554,6 +554,15 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.loadParamsTemplate(filename, **kwargs)
 
+    def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", 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.messageSend(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
+
     def paramsRegisterApp(self, xml, security_limit=-1, app='', callback=None, errback=None):
         if callback is None:
             error_handler = None
@@ -661,15 +670,6 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.saveParamsTemplate(filename, **kwargs)
 
-    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", 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.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
-
     def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None):
         if callback is None:
             error_handler = None
--- a/frontends/src/primitivus/chat.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/primitivus/chat.py	Tue May 24 22:11:04 2016 +0200
@@ -88,7 +88,6 @@
         self.text_list = urwid.ListBox(self.content)
         self.chat_widget = urwid.Frame(self.text_list)
         self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
-        self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
         self.pile = urwid.Pile([self.chat_colums])
         PrimitivusWidget.__init__(self, self.pile, self.target)
 
--- a/frontends/src/primitivus/primitivus	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/primitivus/primitivus	Tue May 24 22:11:04 2016 +0200
@@ -96,12 +96,13 @@
         if self.mode == C.MODE_INSERTION:
             if isinstance(self.host.selected_widget, quick_chat.QuickChat):
                 chat_widget = self.host.selected_widget
-                self.host.sendMessage(chat_widget.target,
-                                     editBar.get_edit_text(),
-                                     mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
-                                     errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
-                                     profile_key=chat_widget.profile
-                                    )
+                self.host.messageSend(
+                    chat_widget.target,
+                    {'': editBar.get_edit_text()}, # TODO: handle language
+                    mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
+                    errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
+                    profile_key=chat_widget.profile
+                    )
                 editBar.set_edit_text('')
         elif self.mode == C.MODE_COMMAND:
             self.commandHandler()
@@ -723,7 +724,9 @@
 
     def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
         super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile)
-        self.contact_lists[profile].setFocus(jid.JID(room_jid_s), True)
+        for contact_list in self.widgets.getWidgets(ContactList):
+            if profile in contact_list.profiles:
+                contact_list.setFocus(jid.JID(room_jid_s), True)
 
     def progressStartedHandler(self, pid, metadata, profile):
         super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile)
--- a/frontends/src/quick_frontend/quick_app.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/quick_frontend/quick_app.py	Tue May 24 22:11:04 2016 +0200
@@ -248,7 +248,7 @@
         self.registerSignal("disconnected")
         self.registerSignal("actionNew")
         self.registerSignal("newContact")
-        self.registerSignal("newMessage")
+        self.registerSignal("messageNew")
         self.registerSignal("newAlert")
         self.registerSignal("presenceUpdate")
         self.registerSignal("subscribe")
@@ -482,10 +482,10 @@
         groups = list(groups)
         self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True)
 
-    def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
+    def messageNewHandler(self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra, profile):
         from_jid = jid.JID(from_jid_s)
         to_jid = jid.JID(to_jid_s)
-        if not self.trigger.point("newMessageTrigger", from_jid, msg, type_, to_jid, extra, profile=profile):
+        if not self.trigger.point("messageNewTrigger", uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile=profile):
             return
 
         from_me = from_jid.bare == self.profiles[profile].whoami.bare
@@ -505,7 +505,8 @@
             contact_list.setContact(from_jid)
 
         # we display the message in the widget
-        chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
+
+        chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
 
         # ContactList alert
         if not from_me:
@@ -520,16 +521,20 @@
             else:
                 contact_list.addAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
 
-    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
+    def messageSend(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
+        if subject is None:
+            subject = {}
+        if extra is None:
+            extra = {}
         if callback is None:
             callback = lambda dummy=None: None # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy
         if errback is None:
             errback = lambda failure: self.showDialog(failure.fullname, failure.message, "error")
 
-        if not self.trigger.point("sendMessageTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key):
+        if not self.trigger.point("messageSendTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key):
             return
 
-        self.bridge.sendMessage(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
+        self.bridge.messageSend(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
 
     def newAlertHandler(self, msg, title, alert_type, profile):
         assert alert_type in ['INFO', 'ERROR']
--- a/frontends/src/quick_frontend/quick_chat.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/quick_frontend/quick_chat.py	Tue May 24 22:11:04 2016 +0200
@@ -25,7 +25,6 @@
 from sat_frontends.quick_frontend.constants import Const as C
 from collections import OrderedDict
 from datetime import datetime
-from time import time
 
 try:
     # FIXME: to be removed when an acceptable solution is here
@@ -52,18 +51,15 @@
         self.nick = None
         self.games = {}  # key=game name (unicode), value=instance of quick_games.RoomGame
 
-        if type_ == C.CHAT_ONE2ONE:
-            self.historyPrint(profile=self.profile)
-
-        # FIXME: has been introduced to temporarily fix http://bugs.goffi.org/show_bug.cgi?id=12
-        self.initialised = False
+        self.historyPrint(profile=self.profile)
 
     def __str__(self):
         return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
 
     @staticmethod
-    def getWidgetHash(target, profile):
-        return (unicode(profile), target.bare)
+    def getWidgetHash(target, profiles):
+        profile = profiles[0]
+        return (profile, target.bare)
 
     @staticmethod
     def getPrivateHash(target, profile):
@@ -99,7 +95,7 @@
         """Tell if this chat widget manage an entity and message type couple
 
         @param entity (jid.JID): (full) jid of the sending entity
-        @param mess_type (str): message type as given by newMessage
+        @param mess_type (str): message type as given by messageNew
         @return (bool): True if this Chat Widget manage this couple
         """
         if self.type == C.CHAT_GROUP:
@@ -112,8 +108,6 @@
 
     def addUser(self, nick):
         """Add user if it is not in the group list"""
-        if not self.initialised:
-            return  # FIXME: tmp fix for bug 12, do not flood the room with the messages when we've just entered it
         self.printInfo("=> %s has joined the room" % nick)
 
     def removeUser(self, nick):
@@ -141,6 +135,7 @@
 
     def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
         """Print the current history
+
         @param size (int): number of messages
         @param search (str): pattern to filter the history results
         @param profile (str): %(doc_profile)s
@@ -152,28 +147,26 @@
 
         target = self.target.bare
 
-        def onHistory(history):
-            self.initialised = True  # FIXME: tmp fix for bug 12
+        def _historyGetCb(history):
             day_format = "%A, %d %b %Y"  # to display the day change
             previous_day = datetime.now().strftime(day_format)
-            for line in history:
-                timestamp, from_jid, to_jid, message, type_, extra = line  # FIXME: extra is unused !
+            for data in history:
+                uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data  # FIXME: extra is unused !
                 if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
                    (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
                     continue
-                message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
+                message_day = datetime.fromtimestamp(timestamp).strftime(day_format)
                 if previous_day != message_day:
                     self.printDayChange(message_day)
                     previous_day = message_day
                 extra["timestamp"] = timestamp
-                self.newMessage(jid.JID(from_jid), target, message, type_, extra, profile)
+                self.messageNew(uid, timestamp, jid.JID(from_jid), target, message, subject, type_, extra, profile)
             self.afterHistoryPrint()
 
-        def onHistoryError(err):
+        def _historyGetEb(err):
             log.error(_("Can't get history"))
 
-        self.initialised = False  # FIXME: tmp fix for bug 12, here needed for :history and :search commands
-        self.host.bridge.getHistory(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=onHistory, errback=onHistoryError)
+        self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb)
 
     def _get_nick(self, entity):
         """Return nick of this entity when possible"""
@@ -195,34 +188,31 @@
         """
         return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again
 
-    def newMessage(self, from_jid, target, msg, type_, extra, profile):
+    def messageNew(self, uid, timestamp, from_jid, target, msg, subject, type_, extra, profile):
+        try:
+            msg = msg.itervalues().next() # FIXME: tmp fix until message refactoring is finished (msg is now a dict)
+        except StopIteration:
+            log.warning(u"No message found (uid: {})".format(uid))
+            msg = ''
         if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
             # we have a private message, we forward it to a private conversation widget
             chat_widget = self.getOrCreatePrivateWidget(target)
-            chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
+            chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
             return
-        try:
-            timestamp = float(extra['timestamp'])
-        except KeyError:
-            timestamp = None
-
-        if not self.initialised and self.type == C.CHAT_ONE2ONE:
-            return  # FIXME: tmp fix for bug 12, do not display the first one2one message which is already in the local history
 
         if type_ == C.MESS_TYPE_INFO:
             self.printInfo(msg, extra=extra)
         else:
-            self.initialised = True  # FIXME: tmp fix for bug 12, do not discard any message from now
-
             nick = self._get_nick(from_jid)
             if msg.startswith('/me '):
-                self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', extra=extra)
+                self.printInfo('* {} {}'.format(nick, msg[4:]), type_='me', extra=extra)
             else:
                 # my_message is True if message comes from local user
                 my_message = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare)
                 self.printMessage(nick, my_message, msg, timestamp, extra, profile)
-        if timestamp:
-            self.afterHistoryPrint()
+        # FIXME: to be checked/removed after message refactoring
+        # if timestamp:
+        self.afterHistoryPrint()
 
     def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
         """Print message in chat window.
@@ -232,11 +222,12 @@
         @param message (unicode): message content
         @param extra (dict): extra data
         """
-        if not timestamp:
-            # XXX: do not send notifications for each line of the history being displayed
-            # FIXME: this must be changed in the future if the timestamp is passed with
-            # all messages and not only with the messages coming from the history.
-            self.notify(nick, message)
+        # FIXME: check/remove this if necessary (message refactoring)
+        # if not timestamp:
+        #     # XXX: do not send notifications for each line of the history being displayed
+        #     # FIXME: this must be changed in the future if the timestamp is passed with
+        #     # all messages and not only with the messages coming from the history.
+        self.notify(nick, message)
 
     def printInfo(self, msg, type_='normal', extra=None):
         """Print general info.
--- a/frontends/src/quick_frontend/quick_contact_list.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/quick_frontend/quick_contact_list.py	Tue May 24 22:11:04 2016 +0200
@@ -549,6 +549,7 @@
             None to count all of them
         @return (list[unicode,None]): list of C.ALERT_* or None for undefined ones
         """
+        return [] # FIXME: temporarily disabled
         if not use_bare_jid:
             alerts = self._alerts.get(entity, [])
         else:
--- a/frontends/src/quick_frontend/quick_widgets.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/frontends/src/quick_frontend/quick_widgets.py	Tue May 24 22:11:04 2016 +0200
@@ -247,8 +247,8 @@
 class QuickWidget(object):
     """generic widget base"""
     SINGLE=True # if True, there can be only one widget per target(s)
-    PROFILES_MULTIPLE=False
-    PROFILES_ALLOW_NONE=False
+    PROFILES_MULTIPLE=False # If True, this widget can handle several profiles at once
+    PROFILES_ALLOW_NONE=False # If True, this widget can be used without profile
 
     def __init__(self, host, target, profiles=None):
         """
--- a/src/bridge/DBus.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/bridge/DBus.py	Tue May 24 22:11:04 2016 +0200
@@ -166,6 +166,11 @@
         pass
 
     @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX,
+                         signature='sdssa{ss}a{ss}sa{ss}s')
+    def messageNew(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile):
+        pass
+
+    @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX,
                          signature='ssss')
     def newAlert(self, message, title, alert_type, profile):
         pass
@@ -176,11 +181,6 @@
         pass
 
     @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX,
-                         signature='ssssa{ss}s')
-    def newMessage(self, from_jid, message, mess_type, to_jid, extra, profile):
-        pass
-
-    @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX,
                          signature='ssss')
     def paramUpdate(self, name, value, category, profile):
         pass
@@ -321,12 +321,6 @@
         return self._callback("getFeatures", unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
-                         in_signature='ssibss', out_signature='a(dssssa{ss})',
-                         async_callbacks=('callback', 'errback'))
-    def getHistory(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None):
-        return self._callback("getHistory", unicode(from_jid), unicode(to_jid), limit, between, unicode(search), unicode(profile), callback=callback, errback=errback)
-
-    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='s',
                          async_callbacks=None)
     def getMainResource(self, contact_jid, profile_key="@DEFAULT@"):
@@ -405,6 +399,12 @@
         return self._callback("getWaitingSub", unicode(profile_key))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='ssibss', out_signature='a(sdssa{ss}a{ss}sa{ss})',
+                         async_callbacks=('callback', 'errback'))
+    def historyGet(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None):
+        return self._callback("historyGet", unicode(from_jid), unicode(to_jid), limit, between, unicode(search), unicode(profile), callback=callback, errback=errback)
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='s', out_signature='b',
                          async_callbacks=None)
     def isConnected(self, profile_key="@DEFAULT@"):
@@ -423,6 +423,12 @@
         return self._callback("loadParamsTemplate", unicode(filename))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='sa{ss}a{ss}sa{ss}s', out_signature='',
+                         async_callbacks=('callback', 'errback'))
+    def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
+        return self._callback("messageSend", unicode(to_jid), message, subject, unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback)
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='sis', out_signature='',
                          async_callbacks=None)
     def paramsRegisterApp(self, xml, security_limit=-1, app=''):
@@ -471,12 +477,6 @@
         return self._callback("saveParamsTemplate", unicode(filename))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
-                         in_signature='ssssa{ss}s', out_signature='',
-                         async_callbacks=('callback', 'errback'))
-    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
-        return self._callback("sendMessage", unicode(to_jid), unicode(message), unicode(subject), unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback)
-
-    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='sssis', out_signature='',
                          async_callbacks=None)
     def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"):
@@ -626,15 +626,15 @@
     def entityDataUpdated(self, jid, name, value, profile):
         self.dbus_bridge.entityDataUpdated(jid, name, value, profile)
 
+    def messageNew(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile):
+        self.dbus_bridge.messageNew(uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile)
+
     def newAlert(self, message, title, alert_type, profile):
         self.dbus_bridge.newAlert(message, title, alert_type, profile)
 
     def newContact(self, contact_jid, attributes, groups, profile):
         self.dbus_bridge.newContact(contact_jid, attributes, groups, profile)
 
-    def newMessage(self, from_jid, message, mess_type, to_jid, extra, profile):
-        self.dbus_bridge.newMessage(from_jid, message, mess_type, to_jid, extra, profile)
-
     def paramUpdate(self, name, value, category, profile):
         self.dbus_bridge.paramUpdate(name, value, category, profile)
 
--- a/src/bridge/bridge_constructor/bridge_template.ini	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Tue May 24 22:11:04 2016 +0200
@@ -34,17 +34,24 @@
 doc_param_2=groups: Roster's groups where the contact is
 doc_param_3=%(doc_profile)s
 
-[newMessage]
+[messageNew]
 type=signal
 category=core
-sig_in=ssssa{ss}s
+sig_in=sdssa{ss}a{ss}sa{ss}s
 doc=A message has been received
-doc_param_0=from_jid: JID where the message is comming from
-doc_param_1=message: Message itself
-doc_param_2=mess_type: Type of the message (cf RFC 6121 §5.2.2) + C.MESS_TYPE_INFO (system info)
+doc_param_0=uid: unique ID of the message (id specific to SàT, this it *NOT* an XMPP id)
+doc_param_1=timestamp: when the message was sent (or declared sent for delayed messages)
+doc_param_2=from_jid: JID where the message is comming from
 doc_param_3=to_jid: JID where the message must be sent
-doc_param_4=extra: extra message information
-doc_param_5=%(doc_profile)s
+doc_param_4=message: message itself, can be in several languages (key is language code or '' for default)
+doc_param_5=subject: subject of the message, can be in several languages (key is language code or '' for default)
+doc_param_6=mess_type: Type of the message (cf RFC 6121 §5.2.2) + C.MESS_TYPE_INFO (system info)
+doc_param_7=extra: extra message information, can have data added by plugins and/or:
+  - thread: id of the thread
+  - thread_parent: id of the parent of the current thread
+  - received_timestamp: date of receiption for delayed messages
+  - delay_sender: entity which has originally sent or which has delayed the message
+doc_param_8=%(doc_profile)s
 
 [newAlert]
 deprecated=
@@ -434,11 +441,11 @@
 doc_return=List of confirmation request data, same as for [askConfirmation]
 
 
-[sendMessage]
+[messageSend]
 async=
 type=method
 category=core
-sig_in=ssssa{ss}s
+sig_in=sa{ss}a{ss}sa{ss}s
 sig_out=
 param_2_default=''
 param_3_default="auto"
@@ -446,8 +453,10 @@
 param_5_default="@NONE@"
 doc=Send a message
 doc_param_0=to_jid: JID of the recipient
-doc_param_1=message: body of the message
-doc_param_2=subject: Subject of the message ('' if no subject)
+doc_param_1=message: body of the message:
+    key is the language of the body, use '' when unknown
+doc_param_2=subject: Subject of the message
+    key is the language of the subkect, use '' when unknown
 doc_param_3=mess_type: Type of the message (cf RFC 6121 §5.2.2) or "auto" for automatic type detection
 doc_param_4=extra: optional data that can be used by a plugin to build more specific messages 
 doc_param_5=%(doc_profile_key)s
@@ -577,12 +586,12 @@
 doc_param_1=%(doc_security_limit)s
 doc_param_2=app: name of the frontend registering the parameters
 
-[getHistory]
+[historyGet]
 async=
 type=method
 category=core
 sig_in=ssibss
-sig_out=a(dssssa{ss})
+sig_out=a(sdssa{ss}a{ss}sa{ss})
 param_3_default=True
 param_4_default=''
 param_5_default="@NONE@"
@@ -593,7 +602,7 @@
 doc_param_3=between: True if we want history between the two jids (in both direction), False if we only want messages from from_jid to to_jid
 doc_param_4=search: pattern to filter the history results
 doc_param_5=%(doc_profile)s
-doc_return=Ordered list (by timestamp) of tuples (timestamp, full from_jid, full to_jid, message, type, extra)
+doc_return=Ordered list (by timestamp) of data as in [messageNew] (without final profile)
 
 [addContact]
 type=method
--- a/src/core/sat_main.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/core/sat_main.py	Tue May 24 22:11:04 2016 +0200
@@ -40,6 +40,7 @@
 import sys
 import os.path
 import uuid
+import time
 
 try:
     from collections import OrderedDict # only available from python 2.7
@@ -85,7 +86,7 @@
         self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses)
         self.bridge.register("getWaitingSub", self.memory.getWaitingSub)
         self.bridge.register("getWaitingConf", self.getWaitingConf)
-        self.bridge.register("sendMessage", self._sendMessage)
+        self.bridge.register("messageSend", self._messageSend)
         self.bridge.register("getConfig", self._getConfig)
         self.bridge.register("setParam", self.setParam)
         self.bridge.register("getParamA", self.memory.getStringParamA)
@@ -94,7 +95,7 @@
         self.bridge.register("getParamsUI", self.memory.getParamsUI)
         self.bridge.register("getParamsCategories", self.memory.getParamsCategories)
         self.bridge.register("paramsRegisterApp", self.memory.paramsRegisterApp)
-        self.bridge.register("getHistory", self.memory.getHistory)
+        self.bridge.register("historyGet", self.memory.historyGet)
         self.bridge.register("setPresence", self._setPresence)
         self.bridge.register("subscription", self.subscription)
         self.bridge.register("addContact", self._addContact)
@@ -527,74 +528,123 @@
             ret.append((conf_id, conf_type, data))
         return ret
 
-    def generateMessageXML(self, mess_data):
-        mess_data['xml'] = domish.Element((None, 'message'))
-        mess_data['xml']["to"] = mess_data["to"].full()
-        mess_data['xml']["from"] = mess_data['from'].full()
-        mess_data['xml']["type"] = mess_data["type"]
-        mess_data['xml']['id'] = str(uuid4())
-        if mess_data["subject"]:
-            mess_data['xml'].addElement("subject", None, mess_data['subject'])
-        if mess_data["message"]: # message without body are used to send chat states
-            mess_data['xml'].addElement("body", None, mess_data["message"])
-        return mess_data
+    def generateMessageXML(self, data):
+        """Generate <message/> stanza from message data
 
-    def _sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key=C.PROF_KEY_NONE):
-        to_jid = jid.JID(to_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.sendMessage(to_jid, msg, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}, profile_key=profile_key)
+        @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['xml:lang'] = lang
+        for lang, message in data["message"].iteritems():
+            body_elt = message_elt.addElement("body", content=message)
+            if lang:
+                body_elt['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 sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE):
-        #FIXME: check validity of recipient
-        profile = self.memory.getProfileName(profile_key)
-        assert profile
-        client = self.profiles[profile]
+    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 = {}
-        mess_data = {  # we put data in a dict, so trigger methods can change them
+        data = {  # dict is similar to the one used in client.onMessage
+            "from": client.jid,
             "to": to_jid,
-            "from": client.jid,
-            "message": msg,
+            "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 mess_data["type"] == "auto":
+        if data["type"] == "auto":
             # we try to guess the type
-            if mess_data["subject"]:
-                mess_data["type"] = 'normal'
-            elif not mess_data["to"].resource:  # if to JID has a resource, the type is not 'groupchat'
+            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(mess_data["to"], ['type'], profile)["type"]
+                    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":
-                    mess_data["type"] = 'groupchat'
+                    data["type"] = 'groupchat'
                 else:
-                    mess_data["type"] = 'chat'
+                    data["type"] = 'chat'
             else:
-                mess_data["type"] == 'chat'
-            mess_data["type"] == "chat" if mess_data["subject"] else "normal"
+                data["type"] == 'chat'
+            data["type"] == "chat" if data["subject"] else "normal"
 
-        send_only = mess_data['extra'].get('send_only', None)
+        # 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', None)
 
         if not no_trigger and not send_only:
-            if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile):
+            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=mess_data["type"], to=to_jid.full()))
+        log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
 
-        def cancelErrorTrap(failure):
-            """A message sending can be cancelled by a plugin treatment"""
-            failure.trap(exceptions.CancelError)
-
-        pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(mess_data))
+        pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
         pre_xml_treatments.chainDeferred(post_xml_treatments)
         post_xml_treatments.addCallback(self._sendMessageToStream, client)
         if send_only:
@@ -602,58 +652,53 @@
         else:
             post_xml_treatments.addCallback(self._storeMessage, client)
             post_xml_treatments.addCallback(self.sendMessageToBridge, client)
-            post_xml_treatments.addErrback(cancelErrorTrap)
-        pre_xml_treatments.callback(mess_data)
+            post_xml_treatments.addErrback(self._cancelErrorTrap)
+        pre_xml_treatments.callback(data)
         return pre_xml_treatments
 
-    def _sendMessageToStream(self, mess_data, client):
+    def _cancelErrorTrap(failure):
+        """A message sending can be cancelled by a plugin treatment"""
+        failure.trap(exceptions.CancelError)
+
+    def _sendMessageToStream(self, data, client):
         """Actualy send the message to the server
 
-        @param mess_data: message data dictionnary
+        @param data: message data dictionnary
         @param client: profile's client
         """
-        client.xmlstream.send(mess_data['xml'])
-        return mess_data
+        client.xmlstream.send(data['xml'])
+        return data
 
-    def _storeMessage(self, mess_data, client):
+    def _storeMessage(self, data, client):
         """Store message into database (for local history)
 
-        @param mess_data: message data dictionnary
+        @param data: message data dictionnary
         @param client: profile's client
         """
-        if mess_data["type"] != "groupchat":
+        if data["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 mess_data['message']: # we need a message to save something
-                self.memory.addToHistory(client.jid, mess_data['to'],
-                                     unicode(mess_data["message"]),
-                                     unicode(mess_data["type"]),
-                                     mess_data['extra'],
-                                     profile=client.profile)
+            if data['message']: # we need a message to save something
+                self.memory.addToHistory(client, data)
             else:
-               log.warning(_("No message found")) # empty body should be managed by plugins before this point
-        return mess_data
+               log.warning(u"No message found") # empty body should be managed by plugins before this point
+        return data
 
-    def sendMessageToBridge(self, mess_data, client):
+    def sendMessageToBridge(self, data, client):
         """Send message to bridge, so frontends can display it
 
-        @param mess_data: message data dictionnary
+        @param data: message data dictionnary
         @param client: profile's client
         """
-        if mess_data["type"] != "groupchat":
-            # we don't send groupchat message back to bridge, as we get them back
+        if data["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 mess_data['message']: # we need a message to save something
-                # We send back the message, so all clients are aware of it
-                self.bridge.newMessage(mess_data['from'].full(),
-                                       unicode(mess_data["message"]),
-                                       mess_type=mess_data["type"],
-                                       to_jid=mess_data['to'].full(),
-                                       extra=mess_data['extra'],
-                                       profile=client.profile)
+            if data['message']: # we need a message to send something
+                # We send back the message, so all frontends are aware of it
+                self.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
             else:
                log.warning(_("No message found"))
-        return mess_data
+        return data
 
     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	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/core/xmpp.py	Tue May 24 22:11:04 2016 +0200
@@ -20,14 +20,20 @@
 from sat.core.i18n import _
 from sat.core.constants import Const as C
 from twisted.internet import task, defer
-from twisted.words.protocols.jabber import jid, xmlstream
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from twisted.words.protocols.jabber import xmlstream
 from twisted.words.protocols.jabber import error
-from wokkel import client, disco, xmppim, generic, delay, iwokkel
+from twisted.words.protocols.jabber import jid
+from twisted.python import failure
+from wokkel import client, disco, xmppim, generic, iwokkel
+from wokkel import delay
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat.core import exceptions
 from zope.interface import implements
-from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+import time
+import calendar
+import uuid
 
 
 class SatXMPPClient(client.XMPPClient):
@@ -42,6 +48,7 @@
         self.__connected = False
         self.profile = profile
         self.host_app = host_app
+        self._mess_id_uid = {} # map from message id to uid use in history. Key: (full_jid,message_id) Value: uid
         self.conn_deferred = defer.Deferred()
         self._waiting_conf = {}  # callback called when a confirmation is received
         self._progress_cb = {}  # callback called when a progress is requested (key = progress id)
@@ -131,53 +138,85 @@
         xmppim.MessageProtocol.__init__(self)
         self.host = host
 
-    def onMessage(self, message):
-        if not message.hasAttribute('from'):
-            message['from'] = self.parent.jid.host
-        log.debug(_(u"got message from: %s") % message["from"])
+    def onMessage(self, message_elt):
+        # TODO: handle threads
+        client = self.parent
+        if not 'from' in message_elt.attributes:
+            message_elt['from'] = client.jid.host
+        log.debug(_(u"got message from: {from_}").format(from_=message_elt['from']))
         post_treat = defer.Deferred() # XXX: plugin can add their treatments to this deferred
 
-        if not self.host.trigger.point("MessageReceived", message, post_treat, profile=self.parent.profile):
+        if not self.host.trigger.point("MessageReceived", client, message_elt, post_treat):
             return
 
-        data = {"from": message['from'],
-                "to": message['to'],
-                "body": "",
-                "extra": {}}
+        message = {}
+        subject = {}
+        extra = {}
+        data = {"from": message_elt['from'],
+                "to": message_elt['to'],
+                "uid": message_elt.getAttribute('uid', unicode(uuid.uuid4())), # XXX: uid is not a standard attribute but may be added by plugins
+                "message": message,
+                "subject": subject,
+                "type": message_elt.getAttribute('type', 'normal'),
+                "extra": extra}
+
+        try:
+            data['stanza_id'] = message_elt['id']
+        except KeyError:
+            pass
+        else:
+            client._mess_id_uid[(data['from'], data['stanza_id'])] = data['uid']
 
-        for e in message.elements():
-            if e.name == "body":
-                data['body'] = e.children[0] if e.children else ""
-            elif e.name == "subject" and e.children:
-                data['extra']['subject'] = e.children[0]
+        # message
+        for e in message_elt.elements(C.NS_CLIENT, 'body'):
+            message[e.getAttribute('xml:lang','')] = unicode(e)
+
+        # subject
+        for e in message_elt.elements(C.NS_CLIENT, 'subject'):
+            subject[e.getAttribute('xml:lang','')] = unicode(e)
 
-        data['type'] = message['type'] if message.hasAttribute('type') else 'normal'
+        # delay and timestamp
+        try:
+            delay_elt = message_elt.elements(delay.NS_DELAY, 'delay').next()
+        except StopIteration:
+            data['timestamp'] = time.time()
+        else:
+            parsed_delay = delay.Delay.fromElement(delay_elt)
+            data['timestamp'] = calendar.timegm(parsed_delay.stamp.utctimetuple())
+            data['received_timestamp'] = unicode(time.time())
+            if parsed_delay.sender:
+                data['delay_sender'] = parsed_delay.sender.full()
+
+        def skipEmptyMessage(data):
+            if not data['message'] and not data['extra']:
+                raise failure.Failure(exceptions.CancelError())
+            return data
 
         def bridgeSignal(data):
+            try:
+                data['extra']['received_timestamp'] = data['received_timestamp']
+                data['extra']['delay_sender'] = data['delay_sender']
+            except KeyError:
+                pass
             if data is not None:
-                self.host.bridge.newMessage(data['from'], data['body'], data['type'], data['to'], data['extra'], profile=self.parent.profile)
+                self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
             return data
 
         def addToHistory(data):
-            try:
-                timestamp = data['extra']['timestamp']  # timestamp added by XEP-0203
-            except KeyError:
-                self.host.memory.addToHistory(jid.JID(data['from']), jid.JID(data['to']), data['body'], data['type'], data['extra'], profile=self.parent.profile)
-            else:
-                if data['type'] != 'groupchat':  # XXX: we don't save delayed messages in history for groupchats
-                    #TODO: add delayed messages to history if they aren't already in it
-                    data['extra']['archive'] = timestamp  # FIXME: this "archive" is actually never used
-                    self.host.memory.addToHistory(jid.JID(data['from']), jid.JID(data['to']), data['body'], data['type'], data['extra'], timestamp, profile=self.parent.profile)
+            data['from'] = jid.JID(data['from'])
+            data['to'] = jid.JID(data['to'])
+            self.host.memory.addToHistory(client, data)
             return data
 
-        def treatmentsEb(failure):
-            failure.trap(exceptions.SkipHistory)
+        def treatmentsEb(failure_):
+            failure_.trap(exceptions.SkipHistory)
             return data
 
-        def cancelErrorTrap(failure):
+        def cancelErrorTrap(failure_):
             """A message sending can be cancelled by a plugin treatment"""
-            failure.trap(exceptions.CancelError)
+            failure_.trap(exceptions.CancelError)
 
+        post_treat.addCallback(skipEmptyMessage)
         post_treat.addCallback(addToHistory)
         post_treat.addErrback(treatmentsEb)
         post_treat.addCallback(bridgeSignal)
@@ -506,7 +545,7 @@
 
     def connectionMade(self):
         log.debug(_(u"Connection made with %s" % self.jabber_host))
-        self.xmlstream.namespace = "jabber:client"
+        self.xmlstream.namespace = C.NS_CLIENT
         self.xmlstream.sendHeader()
 
         iq = xmlstream.IQ(self.xmlstream, 'set')
@@ -526,10 +565,10 @@
         log.debug(_(u"Registration answer: %s") % answer.toXml())
         self.xmlstream.sendFooter()
 
-    def registrationFailure(self, failure):
-        log.info(_("Registration failure: %s") % unicode(failure.value))
+    def registrationFailure(self, failure_):
+        log.info(_("Registration failure: %s") % unicode(failure_.value))
         self.xmlstream.sendFooter()
-        raise failure.value
+        raise failure_.value
 
 
 class SatVersionHandler(generic.VersionHandler):
--- a/src/memory/memory.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/memory/memory.py	Tue May 24 22:11:04 2016 +0200
@@ -510,14 +510,12 @@
 
     ## History ##
 
-    def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE):
-        assert profile != C.PROF_KEY_NONE
-        if extra is None:
-            extra = {}
-        return self.storage.addToHistory(from_jid, to_jid, message, type_, extra, timestamp, profile)
+    def addToHistory(self, client, data):
+        return self.storage.addToHistory(data, client.profile)
 
-    def getHistory(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE):
+    def historyGet(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE):
         """Retrieve messages in history
+
         @param from_jid (JID): source JID (full, or bare for catchall)
         @param to_jid (JID): dest JID (full, or bare for catchall)
         @param limit (int): maximum number of messages to get:
@@ -527,7 +525,7 @@
         @param between (bool): confound source and dest (ignore the direction)
         @param search (str): pattern to filter the history results
         @param profile (str): %(doc_profile)s
-        @return: list of tuple as in http://wiki.goffi.org/wiki/Bridge_API#getHistory
+        @return: list of message data as in [messageNew]
         """
         assert profile != C.PROF_KEY_NONE
         if limit == C.HISTORY_LIMIT_DEFAULT:
@@ -536,7 +534,7 @@
             limit = None
         if limit == 0:
             return defer.succeed([])
-        return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
+        return self.storage.historyGet(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
 
     ## Statuses ##
 
--- a/src/memory/sqlite.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/memory/sqlite.py	Tue May 24 22:11:04 2016 +0200
@@ -27,13 +27,12 @@
 from twisted.enterprise import adbapi
 from twisted.internet import defer
 from collections import OrderedDict
-from time import time
 import re
 import os.path
 import cPickle as pickle
 import hashlib
 
-CURRENT_DB_VERSION = 2
+CURRENT_DB_VERSION = 3
 
 # XXX: DATABASE schemas are used in the following way:
 #      - 'current' key is for the actual database schema, for a new base
@@ -49,9 +48,18 @@
                               ('profiles',        (("id INTEGER PRIMARY KEY ASC", "name TEXT"),
                                                    ("UNIQUE (name)",))),
                               ('message_types',   (("type TEXT PRIMARY KEY",),
-                                                   tuple())),
-                              ('history',         (("id INTEGER PRIMARY KEY ASC", "profile_id INTEGER", "source TEXT", "dest TEXT", "source_res TEXT", "dest_res TEXT", "timestamp DATETIME", "message TEXT", "type TEXT", "extra BLOB"),
-                                                   ("FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE", "FOREIGN KEY(type) REFERENCES message_types(type)"))),
+                                  tuple())),
+                              ('history',         (("uid TEXT PRIMARY KEY", "update_uid TEXT", "profile_id INTEGER", "source TEXT", "dest TEXT", "source_res TEXT", "dest_res TEXT",
+                                                    "timestamp DATETIME NOT NULL", "received_timestamp DATETIME", # XXX: timestamp is the time when the message was emitted. If received time stamp is not NULL, the message was delayed and timestamp is the declared value (and received_timestamp the time of reception)
+                                                    "type TEXT", "extra BLOB"),
+                                                   ("FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE", "FOREIGN KEY(type) REFERENCES message_types(type)",
+                                                    "UNIQUE (profile_id, timestamp, source, dest, source_res, dest_res)" # avoid storing 2 time the same message (specially for delayed cones)
+                                                    ))),
+                              ('message',        (("id INTEGER PRIMARY KEY ASC", "history_uid INTEGER", "message TEXT", "language TEXT"),
+                                                  ("FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE",))),
+                              ('subject',        (("id INTEGER PRIMARY KEY ASC", "history_uid INTEGER", "subject TEXT", "language TEXT"),
+                                                  ("FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE",))),
+                              ('thread',          (("id INTEGER PRIMARY KEY ASC", "history_uid INTEGER", "thread_id TEXT", "parent_id TEXT"),("FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE",))),
                               ('param_gen',       (("category TEXT", "name TEXT", "value TEXT"),
                                                    ("PRIMARY KEY (category,name)",))),
                               ('param_ind',       (("category TEXT", "name TEXT", "profile_id INTEGER", "value TEXT"),
@@ -66,15 +74,26 @@
                                                    ("PRIMARY KEY (namespace, key, profile_id)", "FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE")))
                               )),
                     'INSERT': OrderedDict((
-                              ('message_types', (("'chat'",), ("'error'",), ("'groupchat'",), ("'headline'",), ("'normal'",))),
+                              ('message_types', (("'chat'",),
+                                                 ("'error'",),
+                                                 ("'groupchat'",),
+                                                 ("'headline'",),
+                                                 ("'normal'",),
+                                                 ("'info'",) # info is not standard, but used to keep track of info like join/leave in a MUC
+                                                )),
                               )),
                     },
+        3:         {'specific': 'update_v3'
+                   },
         2:         {'specific': 'update2raw_v2'
                    },
-        1:         {'cols create': {'history': ('extra BLOB',)}
+        1:         {'cols create': {'history': ('extra BLOB',)},
                    },
         }
 
+NOT_IN_EXTRA = ('received_timestamp', 'update_uid') # keys which are in message data extra but not stored in sqlite's extra field
+                                                    # this is specific to this sqlite storage and for now only used for received_timestamp
+                                                    # because this value is stored in a separate field
 
 class SqliteStorage(object):
     """This class manage storage with Sqlite database"""
@@ -114,7 +133,7 @@
 
             if statements is None:
                 return defer.succeed(None)
-            log.debug(u"===== COMMITING STATEMENTS =====\n%s\n============\n\n" % '\n'.join(statements))
+            log.debug(u"===== COMMITTING STATEMENTS =====\n%s\n============\n\n" % '\n'.join(statements))
             d = self.dbpool.runInteraction(self._updateDb, tuple(statements))
             return d
 
@@ -251,57 +270,131 @@
         return d
 
     #History
-    def addToHistory(self, from_jid, to_jid, message, _type='chat', extra=None, timestamp=None, profile=None):
+
+    def _addToHistoryCb(self, dummy, data):
+        # Message metadata were successfuly added to history
+        # now we can add message and subject
+        uid = data['uid']
+        for key in ('message', 'subject'):
+            for lang, value in data[key].iteritems():
+                d = self.dbpool.runQuery("INSERT INTO {key}(history_uid, {key}, language) VALUES (?,?,?)".format(key=key),
+                    (uid, value, lang or None))
+                d.addErrback(lambda dummy: log.error(_(u"Can't save following {key} in history (uid: {uid}, lang:{lang}): {value}".format(
+                    key=key, uid=uid, lang=lang, value=value))))
+        try:
+            thread = data['extra']['thread']
+        except KeyError:
+            pass
+        else:
+            thread_parent = data['extra'].get('thread_parent')
+            d = self.dbpool.runQuery("INSERT INTO thread(history_uid, thread_id, parent_id) VALUES (?,?,?)",
+                (uid, thread, thread_parent))
+            d.addErrback(lambda dummy: log.error(_(u"Can't save following thread in history (uid: {uid}): thread:{thread}), parent:{parent}".format(
+                uid=uid, thread=thread, parent=thread_parent))))
+
+    def _addToHistoryEb(self, failure, data):
+        try:
+            sqlite_msg = failure.value.args[0]
+        except (AttributeError, IndexError):
+            raise failure
+        else:
+            if "UNIQUE constraint failed" in sqlite_msg:
+                log.debug(u"Message is already in history, not storing it again")
+            else:
+                log.error(u"Can't store message in history: {}".format(failure))
+
+    def addToHistory(self, data, profile):
         """Store a new message in history
-        @param from_jid: full source JID
-        @param to_jid: full dest JID
-        @param message: message
-        @param _type: message type (see RFC 6121 §5.2.2)
-        @param extra: dictionary (keys and values are unicode) of extra message data
-        @param timestamp: timestamp in seconds since epoch, or None to use current time
+
+        @param data(dict): message data as build by SatMessageProtocol.onMessage
         """
-        assert(profile)
-        if extra is None:
-            extra = {}
-        extra_ = pickle.dumps({k: v.encode('utf-8') for k, v in extra.items()}, 0).decode('utf-8')
-        d = self.dbpool.runQuery("INSERT INTO history(source, source_res, dest, dest_res, timestamp, message, type, extra, profile_id) VALUES (?,?,?,?,?,?,?,?,?)",
-                                 (from_jid.userhost(), from_jid.resource, to_jid.userhost(), to_jid.resource, timestamp or time(),
-                                  message, _type, extra_, self.profiles[profile]))
-        d.addErrback(lambda ignore: log.error(_(u"Can't save following message in history: from [%(from_jid)s] to [%(to_jid)s] ==> [%(message)s]" %
-                                          {"from_jid": from_jid.full(), "to_jid": to_jid.full(), "message": message})))
+        extra = pickle.dumps({k: v for k, v in data['extra'].iteritems() if k not in NOT_IN_EXTRA}, 0)
+        from_jid = data['from']
+        to_jid = data['to']
+        d = self.dbpool.runQuery("INSERT INTO history(uid, update_uid, profile_id, source, dest, source_res, dest_res, timestamp, received_timestamp, type, extra) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
+                                 (data['uid'], data['extra'].get('update_uid'), self.profiles[profile], data['from'].userhost(), to_jid.userhost(), from_jid.resource, to_jid.resource, data['timestamp'], data.get('received_timestamp'), data['type'], extra))
+        d.addCallbacks(self._addToHistoryCb, self._addToHistoryEb, callbackArgs=[data], errbackArgs=[data])
+        d.addErrback(lambda ignore: log.error(_(u"Can't save following message in history: from [{from_jid}] to [{to_jid}] (uid: {uid})"
+            .format(from_jid=from_jid.full(), to_jid=to_jid.full(), uid=data['uid']))))
         return d
 
-    def getHistory(self, from_jid, to_jid, limit=None, between=True, search=None, profile=None):
+    def sqliteHistoryToList(self, query_result):
+        """Get SQL query result and return a list of message data dicts"""
+        result = []
+        current = {'uid': None}
+        for row in reversed(query_result):
+            uid, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\
+            type_, extra, message, message_lang, subject, subject_lang, thread, thread_parent = row
+            if uid != current['uid']:
+                # new message
+                try:
+                    extra = pickle.loads(str(extra or ""))
+                except EOFError:
+                    extra = {}
+                current = {
+                    'from': "%s/%s" % (source, source_res) if source_res else source,
+                    'to': "%s/%s" % (dest, dest_res) if dest_res else dest,
+                    'uid': uid,
+                    'message': {},
+                    'subject': {},
+                    'type': type_,
+                    'extra': extra,
+                    'timestamp': timestamp,
+                    }
+                if update_uid is not None:
+                    current['extra']['update_uid'] = update_uid
+                if received_timestamp is not None:
+                    current['extra']['received_timestamp'] = str(received_timestamp)
+                result.append(current)
+
+            if message is not None:
+                current['message'][message_lang or ''] = message
+
+            if subject is not None:
+                current['subject'][subject_lang or ''] = subject
+
+            if thread is not None:
+                current_extra = current['extra']
+                current_extra['thread'] = thread
+                if thread_parent is not None:
+                    current_extra['thread_parent'] = thread_parent
+            else:
+                if thread_parent is not None:
+                    log.error(u"Database inconsistency: thread parent without thread (uid: {uid}, thread_parent: {parent})"
+                        .format(uid=uid, parent=thread_parent))
+
+        return result
+
+    def listDict2listTuple(self, messages_data):
+        """Return a list of tuple as used in bridge from a list of messages data"""
+        ret = []
+        for m in messages_data:
+            ret.append((m['uid'], m['timestamp'], m['from'], m['to'], m['message'], m['subject'], m['type'], m['extra']))
+        return ret
+
+    def historyGet(self, from_jid, to_jid, limit=None, between=True, search=None, profile=None):
         """Retrieve messages in history
+
         @param from_jid (JID): source JID (full, or bare for catchall)
         @param to_jid (JID): dest JID (full, or bare for catchall)
         @param limit (int): maximum number of messages to get:
             - 0 for no message (returns the empty list)
             - None for unlimited
         @param between (bool): confound source and dest (ignore the direction)
-        @param search (str): pattern to filter the history results
-        @param profile (str): %(doc_profile)s
-        @return: list of tuple as in http://wiki.goffi.org/wiki/Bridge_API#getHistory
+        @param search (unicode): pattern to filter the history results
+        @param profile (unicode): %(doc_profile)s
+        @return: list of tuple as in [messageNew]
         """
-        assert(profile)
+        assert profile
         if limit == 0:
             return defer.succeed([])
 
-        def sqliteToList(query_result):
-            query_result.reverse()
-            result = []
-            for row in query_result:
-                timestamp, source, source_res, dest, dest_res, message, type_, extra_raw = row
-                try:
-                    extra = pickle.loads(str(extra_raw or ""))
-                except EOFError:
-                    extra = {}
-                result.append((timestamp, "%s/%s" % (source, source_res) if source_res else source,
-                                          "%s/%s" % (dest, dest_res) if dest_res else dest,
-                                          message, type_, extra))
-            return result
-
-        query_parts = ["SELECT timestamp, source, source_res, dest, dest_res, message, type, extra FROM history WHERE profile_id=? AND"]
+        query_parts = ["SELECT uid, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\
+                        type, extra, message, message.language, subject, subject.language, thread_id, thread.parent_id\
+                        FROM history LEFT JOIN message ON history.uid = message.history_uid\
+                        LEFT JOIN subject ON history.uid=subject.history_uid\
+                        LEFT JOIN thread ON history.uid=thread.history_uid\
+                        WHERE profile_id=? AND"] # FIXME: not sure if it's the best request, messages and subjects can appear several times here
         values = [self.profiles[profile]]
 
         def test_jid(type_, _jid):
@@ -324,14 +417,16 @@
             query_parts.append("AND message GLOB ?")
             values.append("*%s*" % search)
 
-        query_parts.append("ORDER BY timestamp DESC")
-
+        query_parts.append("ORDER BY timestamp DESC") # we reverse the order in sqliteHistoryToList
+                                                      # we use DESC here so LIMIT keep the last messages
         if limit is not None:
             query_parts.append("LIMIT ?")
             values.append(limit)
 
         d = self.dbpool.runQuery(" ".join(query_parts), values)
-        return d.addCallback(sqliteToList)
+        d.addCallback(self.sqliteHistoryToList)
+        d.addCallback(self.listDict2listTuple)
+        return d
 
     #Private values
     def loadGenPrivates(self, private_gen, namespace):
@@ -665,6 +760,7 @@
 
     def generateUpdateData(self, old_data, new_data, modify=False):
         """ Generate data for automatic update between two schema data
+
         @param old_data: data of the former schema (which must be updated)
         @param new_data: data of the current schema
         @param modify: if True, always use "cols modify" table, else try to ALTER tables
@@ -728,10 +824,10 @@
     @defer.inlineCallbacks
     def update2raw(self, update, dev_version=False):
         """ Transform update data to raw SQLite statements
+
         @param update: update data as returned by generateUpdateData
         @param dev_version: if True, update will be done in dev mode: no deletion will be done, instead a message will be shown. This prevent accidental lost of data while working on the code/database.
         @return: list of string with SQL statements needed to update the base
-
         """
         ret = self.createData2Raw(update.get('create', {}))
         drop = []
@@ -767,11 +863,104 @@
         specific = update.get('specific', None)
         if specific:
             cmds = yield getattr(self, specific)()
-            ret.extend(cmds)
+            ret.extend(cmds or [])
         defer.returnValue(ret)
 
+    @defer.inlineCallbacks
+    def update_v3(self):
+        """Update database from v2 to v3 (message refactoring)"""
+        # XXX: this update do all the messages in one huge transaction
+        #      this is really memory consuming, but was OK on a reasonably
+        #      big database for tests. If issues are happening, we can cut it
+        #      in smaller transactions using LIMIT and by deleting already updated
+        #      messages
+        log.info(u"Database update to v3, this may take a while")
+
+        # we need to fix duplicate timestamp, as it can result in conflicts with the new schema
+        rows = yield self.dbpool.runQuery("SELECT timestamp, COUNT(*) as c FROM history GROUP BY timestamp HAVING c>1")
+        if rows:
+            log.info("fixing duplicate timestamp")
+            fixed = []
+            for timestamp, dummy in rows:
+                ids_rows = yield self.dbpool.runQuery("SELECT id from history where timestamp=?", (timestamp,))
+                for idx, (id_,) in enumerate(ids_rows):
+                    fixed.append(id_)
+                    yield self.dbpool.runQuery("UPDATE history SET timestamp=? WHERE id=?", (float(timestamp) + idx * 0.001, id_))
+            log.info(u"fixed messages with ids {}".format(u', '.join([unicode(id_) for id_ in fixed])))
+
+        def historySchema(txn):
+            log.info(u"History schema update")
+            txn.execute("ALTER TABLE history RENAME TO tmp_sat_update")
+            txn.execute("CREATE TABLE history (uid TEXT PRIMARY KEY, update_uid TEXT, profile_id INTEGER, source TEXT, dest TEXT, source_res TEXT, dest_res TEXT, timestamp DATETIME NOT NULL, received_timestamp DATETIME, type TEXT, extra BLOB, FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE, FOREIGN KEY(type) REFERENCES message_types(type), UNIQUE (profile_id, timestamp, source, dest, source_res, dest_res))")
+            txn.execute("INSERT INTO history (uid, profile_id, source, dest, source_res, dest_res, timestamp, type, extra) SELECT id, profile_id, source, dest, source_res, dest_res, timestamp, type, extra FROM tmp_sat_update")
+
+        yield self.dbpool.runInteraction(historySchema)
+
+        def newTables(txn):
+            log.info(u"Creating new tables")
+            txn.execute("CREATE TABLE message (id INTEGER PRIMARY KEY ASC, history_uid INTEGER, message TEXT, language TEXT, FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE)")
+            txn.execute("CREATE TABLE thread (id INTEGER PRIMARY KEY ASC, history_uid INTEGER, thread_id TEXT, parent_id TEXT, FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE)")
+            txn.execute("CREATE TABLE subject (id INTEGER PRIMARY KEY ASC, history_uid INTEGER, subject TEXT, language TEXT, FOREIGN KEY(history_uid) REFERENCES history(uid) ON DELETE CASCADE)")
+
+        yield self.dbpool.runInteraction(newTables)
+
+        log.info(u"inserting new message type")
+        yield self.dbpool.runQuery("INSERT INTO message_types VALUES (?)", ('info',))
+
+        log.info(u"messages update")
+        rows = yield self.dbpool.runQuery("SELECT id, timestamp, message, extra FROM tmp_sat_update")
+        total = len(rows)
+
+        def updateHistory(txn, queries):
+            for query, args in iter(queries):
+                txn.execute(query, args)
+
+        queries = []
+        for idx, row in enumerate(rows, 1):
+            if idx % 1000 == 0 or total - idx == 0:
+                log.info("preparing message {}/{}".format(idx, total))
+            id_, timestamp, message, extra = row
+            try:
+                extra = pickle.loads(str(extra or ""))
+            except EOFError:
+                extra = {}
+            except Exception:
+                log.warning(u"Can't handle extra data for message id {}, ignoring it".format(id_))
+                extra = {}
+
+            queries.append(("INSERT INTO message(history_uid, message) VALUES (?,?)", (id_, message)))
+
+            try:
+                subject = extra.pop('subject')
+            except KeyError:
+                pass
+            else:
+                try:
+                    subject = subject.decode('utf-8')
+                except UnicodeEncodeError:
+                    log.warning(u"Error while decoding subject, ignoring it")
+                    del extra['subject']
+                else:
+                    queries.append(("INSERT INTO subject(history_uid, subject) VALUES (?,?)", (id_, subject)))
+
+            received_timestamp = extra.pop('timestamp', None)
+            try:
+                del extra['archive']
+            except KeyError:
+                # archive was not used
+                pass
+
+            queries.append(("UPDATE history SET received_timestamp=?,extra=? WHERE uid=?",(id_, received_timestamp, pickle.dumps(extra, 0))))
+
+        yield self.dbpool.runInteraction(updateHistory, queries)
+
+        log.info("Dropping temporary table")
+        yield self.dbpool.runQuery("DROP TABLE tmp_sat_update")
+        log.info("Database update finished :)")
+
     def update2raw_v2(self):
         """Update the database from v1 to v2 (add passwords encryptions):
+
             - the XMPP password value is re-used for the profile password (new parameter)
             - the profile password is stored hashed
             - the XMPP password is stored encrypted, with the profile password as key
--- a/src/plugins/plugin_exp_command_export.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_exp_command_export.py	Tue May 24 22:11:04 2016 +0200
@@ -18,6 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import _
+from sat.core.constants import Const as C
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from twisted.words.protocols.jabber import jid
@@ -57,10 +58,10 @@
         log.info("connectionMade :)")
 
     def outReceived(self, data):
-        self.parent.host.sendMessage(self.target, self._clean(data), no_trigger=True, profile_key=self.profile)
+        self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile)
 
     def errReceived(self, data):
-        self.parent.host.sendMessage(self.target, self._clean(data), no_trigger=True, profile_key=self.profile)
+        self.parent.host.messageSend(self.target, {'': self._clean(data)}, no_trigger=True, profile_key=self.profile)
 
     def processEnded(self, reason):
         log.info (u"process finished: %d" % (reason.value.exitCode,))
@@ -102,20 +103,19 @@
         except ValueError:
             pass
 
-    def MessageReceivedTrigger(self, message, post_treat, profile):
+    def MessageReceivedTrigger(self, client, message, post_treat):
         """ Check if source is linked and repeat message, else do nothing  """
         from_jid = jid.JID(message["from"])
-        spawned_key = (from_jid.userhostJID(), profile)
-        try:
-            body = [e for e in message.elements() if e.name == 'body'][0]
-        except IndexError:
-            # do not block message without body (chat state notification...)
-            log.debug("No body element found in message, following normal behaviour")
-            return True
-
-        mess_data = unicode(body) + '\n'
+        spawned_key = (from_jid.userhostJID(), client.profile)
 
         if spawned_key in self.spawned:
+            try:
+                body = message.elements(C.NS_CLIENT, 'body').next()
+            except StopIteration:
+                # do not block message without body (chat state notification...)
+                return True
+
+            mess_data = unicode(body) + '\n'
             processes_set = self.spawned[spawned_key]
             _continue = False
             exclusive = False
--- a/src/plugins/plugin_exp_parrot.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_exp_parrot.py	Tue May 24 22:11:04 2016 +0200
@@ -43,22 +43,21 @@
     """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.
-    #     sendMessageTrigger avoid other triggers execution, it's deactivated to allow
+    #     messageSendTrigger avoid other triggers execution, it's deactivated to allow
     #     /unparrot command in text commands plugin.
 
     def __init__(self, host):
         log.info(_("Plugin Parrot initialization"))
         self.host = host
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100)
-        #host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100)
+        #host.trigger.add("messageSend", self.messageSendTrigger, priority=100)
         try:
             self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
         except KeyError:
             log.info(_("Text commands not available"))
 
-    #def sendMessageTrigger(self, mess_data, treatments, profile):
+    #def messageSendTrigger(self, client, mess_data, treatments):
     #    """ Deactivate other triggers if recipient is in parrot links """
-    #    client = self.host.getClient(profile)
     #    try:
     #        _links = client.parrot_links
     #    except AttributeError:
@@ -68,8 +67,10 @@
     #        log.debug("Parrot link detected, skipping other triggers")
     #        raise trigger.SkipOtherTriggers
 
-    def MessageReceivedTrigger(self, message, post_treat, profile):
+    def MessageReceivedTrigger(self, client, message, post_treat):
         """ Check if source is linked and repeat message, else do nothing  """
+        # TODO: many things are not repeated (subject, thread, etc)
+        profile = client.profile
         client = self.host.getClient(profile)
         from_jid = jid.JID(message["from"])
 
@@ -81,28 +82,27 @@
         if not from_jid.userhostJID() in _links:
             return True
 
-        for e in message.elements():
-            if e.name == "body":
-                mess_body = e.children[0] if e.children else ""
+        message = {}
+        for e in message.elements(C.NS_CLIENT, 'body'):
+            body = unicode(e)
+            lang = e.getAttribute('lang') or ''
 
-                try:
-                    entity_type = self.host.memory.getEntityData(from_jid, ['type'], profile)["type"]
-                except (UnknownEntityError, KeyError):
-                    entity_type = "contact"
-                if entity_type == 'chatroom':
-                    src_txt = from_jid.resource
-                    if src_txt == self.host.plugins["XEP-0045"].getRoomNick(from_jid.userhostJID(), profile):
-                        #we won't repeat our own messages
-                        return True
-                else:
-                    src_txt = from_jid.user
-                msg = "[%s] %s" % (src_txt, mess_body)
+            try:
+                entity_type = self.host.memory.getEntityData(from_jid, ['type'], profile)["type"]
+            except (UnknownEntityError, KeyError):
+                entity_type = "contact"
+            if entity_type == 'chatroom':
+                src_txt = from_jid.resource
+                if src_txt == self.host.plugins["XEP-0045"].getRoomNick(from_jid.userhostJID(), profile):
+                    #we won't repeat our own messages
+                    return True
+            else:
+                src_txt = from_jid.user
+            message[lang] = u"[{}] {}".format(src_txt, body)
 
-                linked = _links[from_jid.userhostJID()]
+            linked = _links[from_jid.userhostJID()]
 
-                self.host.sendMessage(jid.JID(unicode(linked)), msg, None, "auto", no_trigger=True, profile_key=profile)
-        else:
-            log.warning("No body element found in message, following normal behaviour")
+            self.host.messageSend(jid.JID(unicode(linked)), message, None, "auto", no_trigger=True, profile_key=profile)
 
         return True
 
--- a/src/plugins/plugin_misc_imap.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_misc_imap.py	Tue May 24 22:11:04 2016 +0200
@@ -154,11 +154,11 @@
         log.debug(u'Mailbox init (%s)' % name)
         if name != "INBOX":
             raise imap4.MailboxException("Only INBOX is managed for the moment")
-        self.mailbox = self.host.plugins["Maildir"].accessMessageBox(name, self.newMessage, profile)
+        self.mailbox = self.host.plugins["Maildir"].accessMessageBox(name, self.messageNew, profile)
 
-    def newMessage(self):
+    def messageNew(self):
         """Called when a new message is in the mailbox"""
-        log.debug("newMessage signal received")
+        log.debug("messageNew signal received")
         nb_messages = self.getMessageCount()
         for listener in self.listeners:
             listener.newMessages(nb_messages, None)
--- a/src/plugins/plugin_misc_maildir.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_misc_maildir.py	Tue May 24 22:11:04 2016 +0200
@@ -98,18 +98,19 @@
         del self.__mailboxes[profile]
         del self.data[profile]
 
-    def messageReceivedTrigger(self, message, post_treat, profile):
+    def messageReceivedTrigger(self, client, message, post_treat):
         """This trigger catch normal message and put the in the Maildir box.
         If the message is not of "normal" type, do nothing
         @param message: message xmlstrem
         @return: False if it's a normal message, True else"""
-        for e in message.elements():
-            if e.name == "body":
-                mess_type = message['type'] if message.hasAttribute('type') else 'normal'
-                if mess_type != 'normal':
-                    return True
-                self.accessMessageBox("INBOX", profile_key=profile).addMessage(message)
-                return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile)
+        profile = client.profile
+        for e in message.elements(C.NS_CLIENT, 'body'):
+            mess_type = message.getAttribute('type', 'normal')
+            if mess_type != 'normal':
+                return True
+            self.accessMessageBox("INBOX", profile_key=profile).addMessage(message)
+            return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile)
+        return True
 
     def accessMessageBox(self, boxname, observer=None, profile_key=C.PROF_KEY_NONE):
         """Create and return a MailboxUser instance
--- a/src/plugins/plugin_misc_smtp.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_misc_smtp.py	Tue May 24 22:11:04 2016 +0200
@@ -87,7 +87,7 @@
         """handle end of message"""
         mail = Parser().parsestr("\n".join(self.message))
         try:
-            self.host._sendMessage(parseaddr(mail['to'].decode('utf-8', 'replace'))[1], mail.get_payload().decode('utf-8', 'replace'),  # TODO: manage other charsets
+            self.host._messageSend(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	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_misc_text_commands.py	Tue May 24 22:11:04 2016 +0200
@@ -58,7 +58,7 @@
     def __init__(self, host):
         log.info(_("Text commands initialization"))
         self.host = host
-        host.trigger.add("sendMessage", self.sendMessageTrigger)
+        host.trigger.add("messageSend", self.messageSendTrigger)
         self._commands = {}
         self._whois = []
         self.registerTextCommands(self)
@@ -166,12 +166,12 @@
         self._whois.append((priority, callback))
         self._whois.sort(key=lambda item: item[0], reverse=True)
 
-    def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
-        """ Install SendMessage command hook """
-        pre_xml_treatments.addCallback(self._sendMessageCmdHook, profile)
+    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+        """Install SendMessage command hook """
+        pre_xml_treatments.addCallback(self._messageSendCmdHook, client)
         return True
 
-    def _sendMessageCmdHook(self, mess_data, profile):
+    def _messageSendCmdHook(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
@@ -179,14 +179,25 @@
         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 sendMessage trigger
+        @param mess_data(dict): data comming from messageSend trigger
         @param profile: %(doc_profile)s
         """
-        msg = mess_data["message"]
+        profile = client.profile
+        try:
+            msg = mess_data["message"]['']
+            msg_lang = ''
+        except KeyError:
+            try:
+                # we have not default message, we try to take the first found
+                msg_lang, msg = mess_data["message"].iteritems().next()
+            except StopIteration:
+                log.debug(u"No message found, skipping text commands")
+                return mess_data
+
         try:
             if msg[:2] == '//':
                 # we have a double '/', it's the escape sequence
-                mess_data["message"] = msg[1:]
+                mess_data["message"][msg_lang] = msg[1:]
                 return mess_data
             if msg[0] != '/':
                 return mess_data
@@ -242,7 +253,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 sendMessage trigger
+        @param mess_data(dict): message data as given in messageSend trigger
         @param cmd_data(dict): command data as returned by self._parseDocString
         @return (bool): True if command can be used in this context
         """
@@ -272,7 +283,7 @@
         else:
             _from = self.host.getJidNStream(profile)[0]
 
-        self.host.bridge.newMessage(unicode(mess_data["to"]), message, C.MESS_TYPE_INFO, unicode(_from), {}, profile=profile)
+        self.host.bridge.messageNew(unicode(mess_data["to"]), {'': message}, {}, C.MESS_TYPE_INFO, unicode(_from), {}, profile=profile)
 
     def cmd_whois(self, mess_data, profile):
         """show informations on entity
--- a/src/plugins/plugin_sec_otr.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_sec_otr.py	Tue May 24 22:11:04 2016 +0200
@@ -32,6 +32,8 @@
 from sat.memory import persistent
 import potr
 import copy
+import time
+import uuid
 
 NS_OTR = "otr_plugin"
 PRIVATE_KEY = "PRIVATE KEY"
@@ -73,11 +75,15 @@
         msg = msg_str.decode('utf-8')
         client = self.user.client
         log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer))
-        mess_data = {'message': msg,
-                     'type': 'chat',
+        mess_data = {
                      'from': client.jid,
-                      'to': self.peer,
-                      'subject': None,
+                     'to': self.peer,
+                     'uid': unicode(uuid.uuid4()),
+                     'message': {'': msg},
+                     'subject': {},
+                     'type': 'chat',
+                     'extra': {},
+                     'timestamp': time.time(),
                     }
         self.host.generateMessageXML(mess_data)
         client.xmlstream.send(mess_data['xml'])
@@ -107,7 +113,7 @@
             return
 
         client = self.user.client
-        self.host.bridge.newMessage(client.jid.full(),
+        self.host.bridge.messageNew(client.jid.full(),
                                     feedback,
                                     mess_type=C.MESS_TYPE_INFO,
                                     to_jid=self.peer.full(),
@@ -202,7 +208,7 @@
         self.context_managers = {}
         self.skipped_profiles = set()
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
-        host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
+        host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
         host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)
         host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE)
         host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
@@ -275,7 +281,7 @@
             log.error(_("jid key is not present !"))
             return defer.fail(exceptions.DataError)
         otrctx = self.context_managers[profile].getContextForUser(to_jid)
-        query = otrctx.sendMessage(0, '?OTRv?')
+        query = otrctx.messageSend(0, '?OTRv?')
         otrctx.inject(query)
         return {}
 
@@ -406,18 +412,21 @@
         encrypted = True
 
         try:
-            res = otrctx.receiveMessage(data['body'].encode('utf-8'))
+            message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict
+            res = otrctx.receiveMessage(message.encode('utf-8'))
         except potr.context.UnencryptedMessage:
             if otrctx.state == potr.context.STATE_ENCRYPTED:
                 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()})
                 client = self.host.getClient(profile)
-                self.host.bridge.newMessage(from_jid.full(),
+                self.host.bridge.messageNew(from_jid.full(),
                                             _(u"WARNING: received unencrypted data in a supposedly encrypted context"),
                                             mess_type=C.MESS_TYPE_INFO,
                                             to_jid=client.jid.full(),
                                             extra={},
                                             profile=client.profile)
             encrypted = False
+        except StopIteration:
+            return data
 
         if not encrypted:
             return data
@@ -425,7 +434,7 @@
             if res[0] != None:
                 # decrypted messages handling.
                 # receiveMessage() will return a tuple, the first part of which will be the decrypted message
-                data['body'] = res[0].decode('utf-8')
+                data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
                 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
             else:
                 raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal)
@@ -433,19 +442,24 @@
     def _receivedTreatmentForSkippedProfiles(self, data, profile):
         """This profile must be skipped because the frontend manages OTR itself,
         but we still need to check if the message must be stored in history or not"""
-        body = data['body'].encode('utf-8')
-        if body.startswith(potr.proto.OTRTAG):
+        try:
+            message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict
+        except StopIteration:
+            return data
+        if message.startswith(potr.proto.OTRTAG):
             raise failure.Failure(exceptions.SkipHistory())
         return data
 
-    def MessageReceivedTrigger(self, message, post_treat, profile):
+    def MessageReceivedTrigger(self, client, message, post_treat):
+        profile = client.profile
         if profile in self.skipped_profiles:
             post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile)
         else:
             post_treat.addCallback(self._receivedTreatment, profile)
         return True
 
-    def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
+    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
+        profile = client.profile
         if profile in self.skipped_profiles:
             return True
         to_jid = copy.copy(mess_data['to'])
@@ -455,13 +469,19 @@
         if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT:
             if otrctx.state == potr.context.STATE_ENCRYPTED:
                 log.debug(u"encrypting message")
-                otrctx.sendMessage(0, mess_data['message'].encode('utf-8'))
-                client = self.host.getClient(profile)
+                try:
+                    msg = mess_data['message']['']
+                except KeyError:
+                    try:
+                        msg = mess_data['message'].itervalues().next()
+                    except StopIteration:
+                        log.warning(u"No message found")
+                        return False
+                otrctx.sendMessage(0, msg.encode('utf-8'))
                 self.host.sendMessageToBridge(mess_data, client)
             else:
                 feedback = D_("Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.")
-                client = self.host.getClient(profile)
-                self.host.bridge.newMessage(to_jid.full(),
+                self.host.bridge.messageNew(to_jid.full(),
                                             feedback,
                                             mess_type=C.MESS_TYPE_INFO,
                                             to_jid=client.jid.full(),
--- a/src/plugins/plugin_xep_0033.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_xep_0033.py	Tue May 24 22:11:04 2016 +0200
@@ -72,11 +72,12 @@
         log.info(_("Extended Stanza Addressing plugin initialization"))
         self.host = host
         self.internal_data = {}
-        host.trigger.add("sendMessage", self.sendMessageTrigger, trigger.TriggerManager.MIN_PRIORITY)
+        host.trigger.add("messageSend", self.messageSendTrigger, trigger.TriggerManager.MIN_PRIORITY)
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
 
-    def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
+    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """Process the XEP-0033 related data to be sent"""
+        profile = client.profile
 
         def treatment(mess_data):
             if not 'address' in mess_data['extra']:
@@ -117,7 +118,7 @@
         Ideas:
         - fix Prosody plugin to check if target server support the feature
         - redesign the database to save only one entry to the database
-        - change the newMessage signal to eventually pass more than one recipient
+        - change the messageNew signal to eventually pass more than one recipient
         """
         def send(mess_data, skip_send=False):
             client = self.host.profiles[profile]
@@ -161,7 +162,7 @@
         d = defer.Deferred().addCallback(lambda dummy: self.internal_data.pop(timestamp))
         defer.DeferredList(defer_list).chainDeferred(d)
 
-    def messageReceivedTrigger(self, message, post_treat, profile):
+    def messageReceivedTrigger(self, client, message, post_treat):
         """In order to save the addressing information in the history"""
         def post_treat_addr(data, addresses):
             data['extra']['addresses'] = ""
@@ -174,9 +175,10 @@
 
         try:
             addresses = message.elements(NS_ADDRESS, 'addresses').next()
-            post_treat.addCallback(post_treat_addr, addresses.children)
         except StopIteration:
             pass  # no addresses
+        else:
+            post_treat.addCallback(post_treat_addr, addresses.children)
         return True
 
     def getHandler(self, profile):
--- a/src/plugins/plugin_xep_0071.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_xep_0071.py	Tue May 24 22:11:04 2016 +0200
@@ -22,6 +22,7 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
+from twisted.internet import defer
 from wokkel import disco, iwokkel
 from zope.interface import implements
 # from lxml import etree
@@ -75,68 +76,108 @@
     def __init__(self, host):
         log.info(_("XHTML-IM plugin initialization"))
         self.host = host
-        self.synt_plg = self.host.plugins["TEXT-SYNTAXES"]
-        self.synt_plg.addSyntax(self.SYNTAX_XHTML_IM, lambda xhtml: xhtml, self.XHTML2XHTML_IM, [self.synt_plg.OPT_HIDDEN])
+        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("sendMessage", self.sendMessageTrigger)
+        host.trigger.add("messageSend", self.messageSendTrigger)
 
     def getHandler(self, profile):
         return XEP_0071_handler(self)
 
-    def _messagePostTreat(self, data, body_elt):
-        """ Callback which manage the post treatment of the message in case of XHTML-IM found
+    def _messagePostTreat(self, data, body_elts):
+        """Callback which manage the post treatment of the message in case of XHTML-IM found
         @param data: data send by MessageReceived trigger through post_treat deferred
-        @param xhtml_im: XHTML-IM body element found
+        @param body_elts: XHTML-IM body elements found
         @return: the data with the extra parameter updated
         """
-        #TODO: check if text only body is empty, then try to convert XHTML-IM to pure text and show a warning message
+        # TODO: check if text only body is empty, then try to convert XHTML-IM to pure text and show a warning message
         def converted(xhtml):
-            data['extra']['xhtml'] = xhtml
-            return data
-        d = self.synt_plg.convert(body_elt.toXml(), self.SYNTAX_XHTML_IM, safe=True)
-        d.addCallback(converted)
-        return d
+            if lang:
+                data['extra']['xhtml_{}'.format(lang)] = xhtml
+            else:
+                data['extra']['xhtml'] = xhtml
 
-    def _sendMessageAddRich(self, mess_data, profile):
+        defers = []
+        for body_elt in body_elts:
+            lang = body_elt.getAttribute('xml:lang', '')
+            d = self._s.convert(body_elt.toXml(), self.SYNTAX_XHTML_IM, safe=True)
+            d.addCallback(converted, lang)
+            defers.append(d)
+
+        d_list = defer.DeferredList(defers)
+        d_list.addCallback(lambda dummy: data)
+        return d_list
+
+    def _messageSendAddRich(self, data, client):
         """ Construct XHTML-IM node and add it XML element
-        @param mess_data: message data as sended by sendMessage callback
+
+        @param data: message data as sended by messageSend callback
         """
-        def syntax_converted(xhtml_im):
-            message_elt = mess_data['xml']
-            html_elt = message_elt.addElement('html', NS_XHTML_IM)
-            body_elt = html_elt.addElement('body', NS_XHTML)
+        # at this point, either ['extra']['rich'] or ['extra']['xhtml'] exists
+        # but both can't exist at the same time
+        message_elt = data['xml']
+        html_elt = message_elt.addElement((NS_XHTML_IM, 'html'))
+
+        def syntax_converted(xhtml_im, lang):
+            body_elt = html_elt.addElement((NS_XHTML, 'body'))
+            if lang:
+                body_elt['xml:lang'] = lang
+                data['extra']['xhtml_{}'.format(lang)] = xhtml_im
+            else:
+                data['extra']['xhtml'] = xhtml_im
             body_elt.addRawXml(xhtml_im)
-            mess_data['extra']['xhtml'] = xhtml_im
-            return mess_data
 
-        syntax = self.synt_plg.getCurrentSyntax(profile)
-        rich = mess_data['extra'].get('rich', '')
-        xhtml = mess_data['extra'].get('xhtml', '')
-        if rich:
-            d = self.synt_plg.convert(rich, syntax, self.SYNTAX_XHTML_IM)
-            if xhtml:
-                raise exceptions.DataError(_("Can't have xhtml and rich content at the same time"))
-        d.addCallback(syntax_converted)
-        return d
+        syntax = self._s.getCurrentSyntax(client.profile)
+        defers = []
+        try:
+            rich = data['extra']['rich']
+        except KeyError:
+            # we have directly XHTML
+            for lang, xhtml in data['extra']['xhtml'].iteritems():
+                d = self._s.convert(xhtml, self._s.SYNTAX_XHTML, self.SYNTAX_XHTML_IM)
+                d.addCallback(syntax_converted, lang)
+                defers.append(d)
+        else:
+            # we have rich syntax to convert
+            for lang, rich_data in rich.iteritems():
+                d = self._s.convert(rich_data, syntax, self.SYNTAX_XHTML_IM)
+                d.addCallback(syntax_converted, lang)
+                defers.append(d)
+        d_list = defer.DeferredList(defers)
+        d_list.addCallback(lambda dummy: data)
+        return d_list
 
-    def messageReceivedTrigger(self, message, post_treat, profile):
+    def messageReceivedTrigger(self, client, message, post_treat):
         """ Check presence of XHTML-IM in message
         """
         try:
             html_elt = message.elements(NS_XHTML_IM, 'html').next()
-            body_elt = html_elt.elements(NS_XHTML, 'body').next()
-            # OK, we have found rich text
-            post_treat.addCallback(self._messagePostTreat, body_elt)
         except StopIteration:
             # No XHTML-IM
             pass
+        else:
+            body_elts = html_elt.elements(NS_XHTML, 'body')
+            post_treat.addCallback(self._messagePostTreat, body_elts)
         return True
 
-    def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
+    def messageSendTrigger(self, client, data, pre_xml_treatments, post_xml_treatments):
         """ Check presence of rich text in extra
         """
-        if 'rich' in mess_data['extra'] or 'xhtml' in mess_data['extra']:
-            post_xml_treatments.addCallback(self._sendMessageAddRich, profile)
+        rich = {}
+        xhtml = {}
+        for key, value in data['extra'].iteritems():
+            if key.startswith('rich'):
+                rich[key[5:]] = value
+            elif key.startswith('xhtml'):
+                xhtml[key[6:]] = value
+        if rich and xhtml:
+            raise exceptions.DataError(_(u"Can't have XHTML and rich content at the same time"))
+        if rich or xhtml:
+            if rich:
+                data['rich'] = rich
+            else:
+                data['xhtml'] = xhtml
+            post_xml_treatments.addCallback(self._messageSendAddRich, client)
         return True
 
     def _purgeStyle(self, styles_raw):
--- a/src/plugins/plugin_xep_0085.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_xep_0085.py	Tue May 24 22:11:04 2016 +0200
@@ -103,7 +103,7 @@
 
         # triggers from core
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
-        host.trigger.add("sendMessage", self.sendMessageTrigger)
+        host.trigger.add("messageSend", self.messageSendTrigger)
         host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger)
 
         # args: to_s (jid as string), profile
@@ -156,11 +156,12 @@
             return False
         return True
 
-    def messageReceivedTrigger(self, message, post_treat, profile):
+    def messageReceivedTrigger(self, client, message, post_treat):
         """
         Update the entity cache when we receive a message with body.
         Check for a chat state in the message and signal frontends.
         """
+        profile = client.profile
         if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile):
             return True
 
@@ -197,11 +198,12 @@
             break
         return True
 
-    def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
+    def messageSendTrigger(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.
         """
+        profile = client.profile
         def treatment(mess_data):
             message = mess_data['xml']
             to_jid = JID(message.getAttribute("to"))
@@ -362,12 +364,15 @@
                 # send a new message without body
                 log.debug(u"sending state '{state}' to {jid}".format(state=state, jid=self.to_jid.full()))
                 client = self.host.getClient(self.profile)
-                mess_data = {'message': None,
-                             'type': self.mess_type,
-                             'from': client.jid,
-                             'to': self.to_jid,
-                             'subject': None
-                             }
+                mess_data = {
+                    'from': client.jid,
+                    'to': self.to_jid,
+                    'uid': '',
+                    'message': {},
+                    'type': self.mess_type,
+                    'subject': {},
+                    'extra': {},
+                    }
                 self.host.generateMessageXML(mess_data)
                 mess_data['xml'].addElement(state, NS_CHAT_STATES)
                 client.xmlstream.send(mess_data['xml'])
--- a/src/plugins/plugin_xep_0203.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_xep_0203.py	Tue May 24 22:11:04 2016 +0200
@@ -22,7 +22,6 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from calendar import timegm
 from wokkel import disco, iwokkel, delay
 try:
     from twisted.words.protocols.xmlstream import XMPPHandler
@@ -49,8 +48,6 @@
     def __init__(self, host):
         log.info(_("Delayed Delivery plugin initialization"))
         self.host = host
-        host.trigger.add("MessageReceived", self.messageReceivedTrigger)
-
 
     def getHandler(self, profile):
         return XEP_0203_handler(self, profile)
@@ -71,30 +68,6 @@
             parent.addChild(elt)
         return elt
 
-    def messagePostTreat(self, data, timestamp):
-        """Set the timestamp of a received message.
-
-        @param data (dict): data send by MessageReceived trigger through post_treat deferred
-        @param timestamp (int): original timestamp of a delayed message
-        @return: dict
-        """
-        data['extra']['timestamp'] = unicode(timestamp)
-        return data
-
-    def messageReceivedTrigger(self, message, post_treat, profile):
-        """Process a delay element from incoming message.
-
-        @param message (domish.Element): message element
-        @param post_treat (Deferred): deferred instance to add post treatments
-        """
-        try:
-            delay_ = delay.Delay.fromElement([elm for elm in message.elements() if elm.name == 'delay'][0])
-        except IndexError:
-            return True
-        else:
-            timestamp = timegm(delay_.stamp.utctimetuple())
-            post_treat.addCallback(self.messagePostTreat, timestamp)
-        return True
 
 class XEP_0203_handler(XMPPHandler):
     implements(iwokkel.IDisco)
--- a/src/plugins/plugin_xep_0334.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/plugins/plugin_xep_0334.py	Tue May 24 22:11:04 2016 +0200
@@ -51,13 +51,13 @@
     def __init__(self, host):
         log.info(_("Message Processing Hints plugin initialization"))
         self.host = host
-        host.trigger.add("sendMessage", self.sendMessageTrigger)
+        host.trigger.add("messageSend", self.messageSendTrigger)
         host.trigger.add("MessageReceived", self.messageReceivedTrigger)
 
     def getHandler(self, profile):
         return XEP_0334_handler(self, profile)
 
-    def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
+    def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
         """Add the hints element to the message to be sent"""
         hints = []
         for key in ('no-permanent-storage', 'no-storage', 'no-copy'):
@@ -78,7 +78,7 @@
             post_xml_treatments.addCallback(treatment)
         return True
 
-    def messageReceivedTrigger(self, message, post_treat, profile):
+    def messageReceivedTrigger(self, client, message, post_treat):
         """Check for hints in the received message"""
         hints = []
         for key in ('no-permanent-storage', 'no-storage'):
--- a/src/test/helpers.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/test/helpers.py	Tue May 24 22:11:04 2016 +0200
@@ -104,7 +104,7 @@
     def registerCallback(self, callback, *args, **kwargs):
         pass
 
-    def sendMessage(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key='@NONE@'):
+    def messageSend(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key='@NONE@'):
         self.sendAndStoreMessage({"to": JID(to_s)})
 
     def _sendMessageToStream(self, mess_data, client):
--- a/src/test/test_core_xmpp.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/test/test_core_xmpp.py	Tue May 24 22:11:04 2016 +0200
@@ -53,7 +53,7 @@
         </message>
         """
         stanza = parseXml(xml)
-        self.host.bridge.expectCall("newMessage", u"sender@example.net/house", u"test", u"chat", u"test@example.org/SàT", {}, profile=Const.PROFILE[0])
+        self.host.bridge.expectCall("messageNew", u"sender@example.net/house", u"test", u"chat", u"test@example.org/SàT", {}, profile=Const.PROFILE[0])
         self.message.onMessage(stanza)
 
 
--- a/src/test/test_plugin_xep_0033.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/test/test_plugin_xep_0033.py	Tue May 24 22:11:04 2016 +0200
@@ -27,7 +27,6 @@
 from twisted.internet import defer
 from wokkel.generic import parseXml
 from twisted.words.protocols.jabber.jid import JID
-from logging import ERROR
 
 PROFILE_INDEX = 0
 PROFILE = Const.PROFILE[PROFILE_INDEX]
@@ -60,7 +59,7 @@
         """ % (JID_STR_FROM, JID_STR_TO, JID_STR_X_TO, JID_STR_X_CC, JID_STR_X_BCC)
         stanza = parseXml(xml.encode("utf-8"))
         treatments = defer.Deferred()
-        self.plugin.messageReceivedTrigger(stanza, treatments, PROFILE)
+        self.plugin.messageReceivedTrigger(self.host.getClient(PROFILE), stanza, treatments)
         data = {'extra': {}}
 
         def cb(data):
@@ -87,7 +86,7 @@
         return mess_data
 
     def _assertAddresses(self, mess_data):
-        """The mess_data that we got here has been modified by self.plugin.sendMessageTrigger,
+        """The mess_data that we got here has been modified by self.plugin.messageSendTrigger,
         check that the addresses element has been added to the stanza."""
         expected = self._get_mess_data()['xml']
         addresses_extra = """
@@ -135,28 +134,28 @@
         return defer.DeferredList(d_list).addCallback(cb_list)
 
     def _trigger(self, data):
-        """Execute self.plugin.sendMessageTrigger with a different logging
+        """Execute self.plugin.messageSendTrigger with a different logging
         level to not pollute the output, then check that the plugin did its
         job. It should abort sending the message or add the extended
         addressing information to the stanza.
-        @param data: the data to be processed by self.plugin.sendMessageTrigger
+        @param data: the data to be processed by self.plugin.messageSendTrigger
         """
         pre_treatments = defer.Deferred()
         post_treatments = defer.Deferred()
         helpers.muteLogging()
-        self.plugin.sendMessageTrigger(data, pre_treatments, post_treatments, PROFILE)
+        self.plugin.messageSendTrigger(self.host.getClient[PROFILE], data, pre_treatments, post_treatments)
         post_treatments.callback(data)
         helpers.unmuteLogging()
         post_treatments.addCallbacks(self._assertAddresses, lambda failure: failure.trap(CancelError))
         return post_treatments
 
-    def test_sendMessageTriggerFeatureNotSupported(self):
+    def test_messageSendTriggerFeatureNotSupported(self):
         # feature is not supported, abort the message
         self.host.memory.reinit()
         data = self._get_mess_data()
         return self._trigger(data)
 
-    def test_sendMessageTriggerFeatureSupported(self):
+    def test_messageSendTriggerFeatureSupported(self):
         # feature is supported by the main target server
         self.host.reinit()
         self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE)
@@ -164,7 +163,7 @@
         d = self._trigger(data)
         return d.addCallback(lambda dummy: self._checkSentAndStored())
 
-    def test_sendMessageTriggerFeatureFullySupported(self):
+    def test_messageSendTriggerFeatureFullySupported(self):
         # feature is supported by all target servers
         self.host.reinit()
         self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE)
@@ -174,7 +173,7 @@
         d = self._trigger(data)
         return d.addCallback(lambda dummy: self._checkSentAndStored())
 
-    def test_sendMessageTriggerFixWrongEntity(self):
+    def test_messageSendTriggerFixWrongEntity(self):
         # check that a wrong recipient entity is fixed by the backend
         self.host.reinit()
         self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE)
--- a/src/test/test_plugin_xep_0085.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/test/test_plugin_xep_0085.py	Tue May 24 22:11:04 2016 +0200
@@ -49,9 +49,9 @@
                    state, plugin.NS_CHAT_STATES)
             stanza = parseXml(xml.encode("utf-8"))
             self.host.bridge.expectCall("chatStateReceived", Const.JID_STR[1], state, Const.PROFILE[0])
-            self.plugin.messageReceivedTrigger(stanza, None, Const.PROFILE[0])
+            self.plugin.messageReceivedTrigger(self.host.getClient(Const.PROFILE[0]), stanza, None)
 
-    def test_sendMessageTrigger(self):
+    def test_messageSendTrigger(self):
         def cb(data):
             xml = data['xml'].toXml().encode("utf-8")
             self.assertEqualXML(xml, expected.toXml().encode("utf-8"))
@@ -73,7 +73,7 @@
             expected = deepcopy(mess_data['xml'])
             expected.addElement(state, plugin.NS_CHAT_STATES)
             post_treatments = defer.Deferred()
-            self.plugin.sendMessageTrigger(mess_data, None, post_treatments, Const.PROFILE[0])
+            self.plugin.messageSendTrigger(self.host.getClient(Const.PROFILE[0]), mess_data, None, post_treatments)
 
             post_treatments.addCallback(cb)
             post_treatments.callback(mess_data)
--- a/src/test/test_plugin_xep_0334.py	Mon Apr 18 18:35:19 2016 +0200
+++ b/src/test/test_plugin_xep_0334.py	Tue May 24 22:11:04 2016 +0200
@@ -36,7 +36,7 @@
         self.host = helpers.FakeSAT()
         self.plugin = XEP_0334(self.host)
 
-    def test_sendMessageTrigger(self):
+    def test_messageSendTrigger(self):
         template_xml = """
         <message
             from='romeo@montague.net/orchard'
@@ -59,7 +59,7 @@
                          'extra': {key: True}
                          }
             treatments = defer.Deferred()
-            self.plugin.sendMessageTrigger(mess_data, defer.Deferred(), treatments, C.PROFILE[0])
+            self.plugin.messageSendTrigger(self.host.getClient(C.PROFILE[0]), mess_data, defer.Deferred(), treatments)
             if treatments.callbacks:  # the trigger added a callback
                 expected_xml = template_xml % ('<%s xmlns="urn:xmpp:hints"/>' % key)
                 treatments.addCallback(cb, expected_xml)
@@ -90,7 +90,7 @@
         for key in (HINTS + ('dummy_hint',)):
             message = parseXml(template_xml % ('<%s xmlns="urn:xmpp:hints"/>' % key))
             post_treat = defer.Deferred()
-            self.plugin.messageReceivedTrigger(message, post_treat, C.PROFILE[0])
+            self.plugin.messageReceivedTrigger(self.host.getClient(C.PROFILE[0]), message, post_treat)
             if post_treat.callbacks:
                 assert(key in ('no-permanent-storage', 'no-storage'))
                 post_treat.addCallbacks(cb, eb)