Launchpad automatic translations update.
[odoo/odoo.git] / addons / mail / mail_thread.py
index 050c755..15bc532 100644 (file)
@@ -36,66 +36,10 @@ from tools.safe_eval import safe_eval as eval
 
 _logger = logging.getLogger(__name__)
 
+
 def decode_header(message, header, separator=' '):
     return separator.join(map(decode, message.get_all(header, [])))
 
-class many2many_reference(fields.many2many):
-    """ many2many_reference manages many2many fields where one id is found
-        by a reference-like key (a char column in addition to the foreign id).
-        The reference_column attribute on the many2many fields is used;
-        if not defined, ``res_model`` is used. """
-
-    def _get_query_and_where_params(self, cr, model, ids, values, where_params):
-        """ Add in where condition like mail_followers.res_model = 'crm.lead' """
-        reference_column = self.reference_column if self.reference_column else 'res_model'
-        values.update(reference_column=reference_column, reference_value=model._name)
-        query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
-                    FROM %(rel)s, %(from_c)s \
-                    WHERE %(rel)s.%(id1)s IN %%s \
-                    AND %(rel)s.%(id2)s = %(tbl)s.id \
-                    AND %(rel)s.%(reference_column)s = \'%(reference_value)s\' \
-                    %(where_c)s  \
-                    %(order_by)s \
-                    %(limit)s \
-                    OFFSET %(offset)d' \
-                % values
-        return query, where_params
-
-    def set(self, cr, model, id, name, values, user=None, context=None):
-        """ Override to add the reference field in queries. """
-        if not values: return
-        rel, id1, id2 = self._sql_names(model)
-        obj = model.pool.get(self._obj)
-        # reference column name: given by attribute or res_model
-        reference_column = self.reference_column if self.reference_column else 'res_model'
-        for act in values:
-            if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
-                continue
-            if act[0] == 0:
-                idnew = obj.create(cr, user, act[2], context=context)
-                cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, idnew, model._name))
-            elif act[0] == 3:
-                cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name))
-            elif act[0] == 4:
-                # following queries are in the same transaction - so should be relatively safe
-                cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name))
-                if not cr.fetchone():
-                    cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act[1], model._name))
-            elif act[0] == 5:
-                cr.execute('delete from '+rel+' where '+id1+' = %s AND '+reference_column+'=%s', (id, model._name))
-            elif act[0] == 6:
-                d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
-                if d1:
-                    d1 = ' and ' + ' and '.join(d1)
-                else:
-                    d1 = ''
-                cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+reference_column+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2)
-                for act_nbr in act[2]:
-                    cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act_nbr, model._name))
-            # cases 1, 2: performs write and unlink -> default implementation is ok
-            else:
-                return super(many2many_reference, self).set(cr, model, id, name, values, user, context)
-
 
 class mail_thread(osv.AbstractModel):
     ''' mail_thread model is meant to be inherited by any model that needs to
@@ -114,40 +58,45 @@ class mail_thread(osv.AbstractModel):
         to override at least the ``message_new`` and ``message_update``
         methods (calling ``super``) to add model-specific behavior at
         creation and update of a thread when processing incoming emails.
+
+        Options:
+            - _mail_flat_thread: if set to True, all messages without parent_id
+                are automatically attached to the first message posted on the
+                ressource. If set to False, the display of Chatter is done using
+                threads, and no parent_id is automatically set.
     '''
     _name = 'mail.thread'
     _description = 'Email Thread'
-    _mail_autothread = True
+    _mail_flat_thread = True
 
     def _get_message_data(self, cr, uid, ids, name, args, context=None):
         """ Computes:
             - message_unread: has uid unread message for the document
             - message_summary: html snippet summarizing the Chatter for kanban views """
         res = dict((id, dict(message_unread=False, message_summary='')) for id in ids)
+        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
 
-        # search for unread messages, by reading directly mail.notification, as SUPERUSER
-        notif_obj = self.pool.get('mail.notification')
-        notif_ids = notif_obj.search(cr, SUPERUSER_ID, [
-            ('partner_id.user_ids', 'in', [uid]),
-            ('message_id.res_id', 'in', ids),
-            ('message_id.model', '=', self._name),
-            ('read', '=', False)
-        ], context=context)
-        for notif in notif_obj.browse(cr, SUPERUSER_ID, notif_ids, context=context):
-            res[notif.message_id.res_id]['message_unread'] = True
+        # search for unread messages, directly in SQL to improve performances
+        cr.execute("""  SELECT m.res_id FROM mail_message m
+                        RIGHT JOIN mail_notification n
+                        ON (n.message_id = m.id AND n.partner_id = %s AND (n.read = False or n.read IS NULL))
+                        WHERE m.model = %s AND m.res_id in %s""",
+                    (user_pid, self._name, tuple(ids),))
+        msg_ids = [result[0] for result in cr.fetchall()]
+        for msg_id in msg_ids:
+            res[msg_id]['message_unread'] = True
 
         for thread in self.browse(cr, uid, ids, context=context):
             cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or ''
             res[thread.id]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread.message_comment_ids), len(thread.message_follower_ids))
 
         return res
-        
+
     def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
         """ Computes:
-            - message_is_follower: is uid in the document followers
             - message_subtype_data: data about document subtypes: which are
                 available, which are followed if any """
-        res = dict((id, dict(message_subtype_data='', message_is_follower=False)) for id in ids)
+        res = dict((id, dict(message_subtype_data='')) for id in ids)
         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
 
         # find current model subtypes, add them to a dictionary
@@ -166,11 +115,10 @@ class mail_thread(osv.AbstractModel):
         ], context=context)
         for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
             thread_subtype_dict = res[fol.res_id]['message_subtype_data']
-            res[fol.res_id]['message_is_follower'] = True
             for subtype in fol.subtype_ids:
                 thread_subtype_dict[subtype.name]['followed'] = True
             res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
-        
+
         return res
 
     def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None):
@@ -186,17 +134,73 @@ class mail_thread(osv.AbstractModel):
             res[notif.message_id.res_id] = True
         return [('id', 'in', res.keys())]
 
+    def _get_followers(self, cr, uid, ids, name, arg, context=None):
+        fol_obj = self.pool.get('mail.followers')
+        fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids)])
+        res = dict((id, dict(message_follower_ids=[], message_is_follower=False)) for id in ids)
+        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids):
+            res[fol.res_id]['message_follower_ids'].append(fol.partner_id.id)
+            if fol.partner_id.id == user_pid:
+                res[fol.res_id]['message_is_follower'] = True
+        return res
+
+    def _set_followers(self, cr, uid, id, name, value, arg, context=None):
+        if not value:
+            return
+        partner_obj = self.pool.get('res.partner')
+        fol_obj = self.pool.get('mail.followers')
+
+        # read the old set of followers, and determine the new set of followers
+        fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', '=', id)])
+        old = set(fol.partner_id.id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids))
+        new = set(old)
+
+        for command in value or []:
+            if isinstance(command, (int, long)):
+                new.add(command)
+            elif command[0] == 0:
+                new.add(partner_obj.create(cr, uid, command[2], context=context))
+            elif command[0] == 1:
+                partner_obj.write(cr, uid, [command[1]], command[2], context=context)
+                new.add(command[1])
+            elif command[0] == 2:
+                partner_obj.unlink(cr, uid, [command[1]], context=context)
+                new.discard(command[1])
+            elif command[0] == 3:
+                new.discard(command[1])
+            elif command[0] == 4:
+                new.add(command[1])
+            elif command[0] == 5:
+                new.clear()
+            elif command[0] == 6:
+                new = set(command[2])
+
+        # remove partners that are no longer followers
+        fol_ids = fol_obj.search(cr, SUPERUSER_ID,
+            [('res_model', '=', self._name), ('res_id', '=', id), ('partner_id', 'not in', list(new))])
+        fol_obj.unlink(cr, SUPERUSER_ID, fol_ids)
+
+        # add new followers
+        for partner_id in new - old:
+            fol_obj.create(cr, SUPERUSER_ID, {'res_model': self._name, 'res_id': id, 'partner_id': partner_id})
+
+    def _search_followers(self, cr, uid, obj, name, args, context):
+        fol_obj = self.pool.get('mail.followers')
+        res = []
+        for field, operator, value in args:
+            assert field == name
+            fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)])
+            res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)]
+            res.append(('id', 'in', res_ids))
+        return res
+
     _columns = {
-        'message_is_follower': fields.function(_get_subscription_data,
-            type='boolean', string='Is a Follower', multi='_get_subscription_data,'),
-        'message_subtype_data': fields.function(_get_subscription_data,
-            type='text', string='Subscription data', multi="_get_subscription_data",
-            help="Holds data about the subtypes. The content of this field "\
-                  "is a structure holding the current model subtypes, and the "\
-                  "current document followed subtypes."),
-        'message_follower_ids': many2many_reference('res.partner',
-            'mail_followers', 'res_id', 'partner_id',
-            reference_column='res_model', string='Followers'),
+        'message_is_follower': fields.function(_get_followers,
+            type='boolean', string='Is a Follower', multi='_get_followers,'),
+        'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
+                fnct_search=_search_followers, type='many2many',
+                obj='res.partner', string='Followers', multi='_get_followers'),
         'message_comment_ids': fields.one2many('mail.message', 'res_id',
             domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))],
             string='Comments and emails',
@@ -315,10 +319,12 @@ class mail_thread(osv.AbstractModel):
         """
         assert isinstance(message, Message), 'message must be an email.message.Message at this point'
         message_id = message.get('Message-Id')
+        references = decode_header(message, 'References')
+        in_reply_to = decode_header(message, 'In-Reply-To')
 
         # 1. Verify if this is a reply to an existing thread
-        references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To')
-        ref_match = references and tools.reference_re.search(references)
+        thread_references = references or in_reply_to
+        ref_match = thread_references and tools.reference_re.search(thread_references)
         if ref_match:
             thread_id = int(ref_match.group(1))
             model = ref_match.group(2) or model
@@ -329,6 +335,14 @@ class mail_thread(osv.AbstractModel):
                               message_id, model, thread_id, custom_values, uid)
                 return [(model, thread_id, custom_values, uid)]
 
+        # Verify this is a reply to a private message
+        message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context)
+        if message_ids:
+            message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
+            _logger.debug('Routing mail with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
+                            message_id, message.id, custom_values, uid)
+            return [(message.model, message.res_id, custom_values, uid)]
+
         # 2. Look for a matching mail.alias entry
         # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
         # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
@@ -372,14 +386,19 @@ class mail_thread(osv.AbstractModel):
     def message_process(self, cr, uid, model, message, custom_values=None,
                         save_original=False, strip_attachments=False,
                         thread_id=None, context=None):
-        """Process an incoming RFC2822 email message, relying on
-           ``mail.message.parse()`` for the parsing operation,
-           and ``message_route()`` to figure out the target model.
+        """ Process an incoming RFC2822 email message, relying on
+            ``mail.message.parse()`` for the parsing operation,
+            and ``message_route()`` to figure out the target model.
 
-           Once the target model is known, its ``message_new`` method
-           is called with the new message (if the thread record did not exist)
+            Once the target model is known, its ``message_new`` method
+            is called with the new message (if the thread record did not exist)
             or its ``message_update`` method (if it did).
 
+            There is a special case where the target model is False: a reply
+            to a private message. In this case, we skip the message_new /
+            message_update step, to just post a new message using mail_thread
+            message_post.
+
            :param string model: the fallback model to use if the message
                does not match any of the currently configured mail aliases
                (may be None if a matching alias is supposed to be present)
@@ -398,7 +417,8 @@ class mail_thread(osv.AbstractModel):
                overrides the automatic detection based on the message
                headers.
         """
-        if context is None: context = {}
+        if context is None:
+            context = {}
 
         # extract message bytes - we are forced to pass the message as binary because
         # we don't know its encoding until we parse its headers and hence can't
@@ -414,20 +434,25 @@ class mail_thread(osv.AbstractModel):
                                     thread_id, custom_values,
                                     context=context)
         msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
-        if strip_attachments: msg.pop('attachments', None)
+        if strip_attachments:
+            msg.pop('attachments', None)
         thread_id = False
         for model, thread_id, custom_values, user_id in routes:
             if self._name != model:
                 context.update({'thread_model': model})
-            model_pool = self.pool.get(model)
-            assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
-                "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
-                    (msg['message-id'], model)
-            if thread_id and hasattr(model_pool, 'message_update'):
-                model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
+            if model:
+                model_pool = self.pool.get(model)
+                assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
+                    "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
+                        (msg['message_id'], model)
+                if thread_id and hasattr(model_pool, 'message_update'):
+                    model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
+                else:
+                    thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
             else:
-                thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
-            self.message_post(cr, uid, [thread_id], context=context, **msg)
+                assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
+                model_pool = self.pool.get('mail.thread')
+            model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg)
         return thread_id
 
     def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
@@ -495,7 +520,7 @@ class mail_thread(osv.AbstractModel):
             body = tools.ustr(body, encoding, errors='replace')
             if message.get_content_type() == 'text/plain':
                 # text/plain -> <pre/>
-                body = tools.append_content_to_html(u'', body)
+                body = tools.append_content_to_html(u'', body, preserve=True)
         else:
             alternative = (message.get_content_type() == 'multipart/alternative')
             for part in message.walk():
@@ -510,7 +535,7 @@ class mail_thread(osv.AbstractModel):
                 # 2) text/plain -> <pre/>
                 if part.get_content_type() == 'text/plain' and (not alternative or not body):
                     body = tools.append_content_to_html(body, tools.ustr(part.get_payload(decode=True),
-                                                                         encoding, errors='replace'))
+                                                                         encoding, errors='replace'), preserve=True)
                 # 3) text/html -> raw
                 elif part.get_content_type() == 'text/html':
                     html = tools.ustr(part.get_payload(decode=True), encoding, errors='replace')
@@ -538,7 +563,7 @@ class mail_thread(osv.AbstractModel):
                     field may not be present if missing in original
                     message::
 
-                    { 'message-id': msg_id,
+                    { 'message_id': msg_id,
                       'subject': subject,
                       'from': from,
                       'to': to,
@@ -548,7 +573,10 @@ class mail_thread(osv.AbstractModel):
                                       ('file2', 'bytes')}
                     }
         """
-        msg_dict = {}
+        msg_dict = {
+            'type': 'email',
+            'author_id': False,
+        }
         if not isinstance(message, Message):
             if isinstance(message, unicode):
                 # Warning: message_from_string doesn't always work correctly on unicode,
@@ -566,7 +594,7 @@ class mail_thread(osv.AbstractModel):
         if 'Subject' in message:
             msg_dict['subject'] = decode(message.get('Subject'))
 
-        # Envelope fields not stored in  mail.message but made available for message_new()
+        # Envelope fields not stored in mail.message but made available for message_new()
         msg_dict['from'] = decode(message.get('from'))
         msg_dict['to'] = decode(message.get('to'))
         msg_dict['cc'] = decode(message.get('cc'))
@@ -575,8 +603,10 @@ class mail_thread(osv.AbstractModel):
             author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
             if author_ids:
                 msg_dict['author_id'] = author_ids[0]
+            else:
+                msg_dict['email_from'] = message.get('from')
         partner_ids = self._message_find_partners(cr, uid, message, ['From', 'To', 'Cc'], context=context)
-        msg_dict['partner_ids'] = partner_ids
+        msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
 
         if 'Date' in message:
             date_hdr = decode(message.get('Date'))
@@ -617,22 +647,28 @@ class mail_thread(osv.AbstractModel):
             mail.message ID. Extra keyword arguments will be used as default
             column values for the new mail.message record.
             Auto link messages for same id and object
-            :param int thread_id: thread ID to post into, or list with one ID
+            :param int thread_id: thread ID to post into, or list with one ID;
+                if False/0, mail.message model will also be set as False
             :param str body: body of the message, usually raw HTML that will
                 be sanitized
             :param str subject: optional subject
             :param str type: mail_message.type
             :param int parent_id: optional ID of parent message in this thread
-            :param tuple(str,str) attachments: list of attachment tuples in the form
+            :param tuple(str,str) attachments or list id: list of attachment tuples in the form
                 ``(name,content)``, where content is NOT base64 encoded
             :return: ID of newly created mail.message
         """
-        context = context or {}
-        attachments = attachments or []
+        if context is None:
+            context = {}
+        if attachments is None:
+            attachments = {}
+
         assert (not thread_id) or isinstance(thread_id, (int, long)) or \
-            (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id"
+            (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id; should be 0, False, an ID or a list with one ID"
         if isinstance(thread_id, (list, tuple)):
             thread_id = thread_id and thread_id[0]
+        mail_message = self.pool.get('mail.message')
+        model = context.get('thread_model', self._name) if thread_id else False
 
         attachment_ids = []
         for name, content in attachments:
@@ -648,24 +684,31 @@ class mail_thread(osv.AbstractModel):
             }
             attachment_ids.append((0, 0, data_attach))
 
-        # get subtype
-        if not subtype:
-            subtype = 'mail.mt_comment'
-        s = subtype.split('.')
-        if len(s)==1:
-            s = ('mail', s[0])
-        ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s[0], s[1])
-        subtype_id = ref and ref[1] or False
-
-        model = context.get('thread_model', self._name) if thread_id else False
-        messages = self.pool.get('mail.message')
-
-        #auto link messages for same id and object
-        if self._mail_autothread and thread_id:
-            message_ids = messages.search(cr, uid, ['&',('res_id', '=', thread_id),('model','=',model)], context=context)
-            if len(message_ids):
-                parent_id = min(message_ids)
-
+        # fetch subtype
+        if subtype:
+            s_data = subtype.split('.')
+            if len(s_data) == 1:
+                s_data = ('mail', s_data[0])
+            ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s_data[0], s_data[1])
+            subtype_id = ref and ref[1] or False
+        else:
+            subtype_id = False
+
+        # _mail_flat_thread: automatically set free messages to the first posted message
+        if self._mail_flat_thread and not parent_id and thread_id:
+            message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model)], context=context, order="id ASC", limit=1)
+            parent_id = message_ids and message_ids[0] or False
+        # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
+        elif parent_id:
+            message_ids = mail_message.search(cr, SUPERUSER_ID, [('id', '=', parent_id), ('parent_id', '!=', False)], context=context)
+            # avoid loops when finding ancestors
+            processed_list = []
+            if message_ids:
+                message = mail_message.browse(cr, SUPERUSER_ID, message_ids[0], context=context)
+                while (message.parent_id and message.parent_id.id not in processed_list):
+                    processed_list.append(message.parent_id.id)
+                    message = message.parent_id
+                parent_id = message.id
 
         values = kwargs
         values.update({
@@ -679,43 +722,81 @@ class mail_thread(osv.AbstractModel):
             'subtype_id': subtype_id,
         })
 
-        if parent_id:
-            msg = messages.browse(cr, uid, parent_id, context=context)
-            if msg.is_private:
-                values["is_private"] = msg.is_private
-
         # Avoid warnings about non-existing fields
         for x in ('from', 'to', 'cc'):
             values.pop(x, None)
 
-        return messages.create(cr, uid, values, context=context)
+        return mail_message.create(cr, uid, values, context=context)
+
+    def message_post_user_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False,
+                                attachment_ids=None, context=None, content_subtype='plaintext', **kwargs):
+        """ Wrapper on message_post, used for user input :
+            - mail gateway
+            - quick reply in Chatter (refer to mail.js), not
+                the mail.compose.message wizard
+            The purpose is to perform some pre- and post-processing:
+            - if body is plaintext: convert it into html
+            - if parent_id: handle reply to a previous message by adding the
+                parent partners to the message
+            - type and subtype: comment and mail.mt_comment by default
+            - attachment_ids: supposed not attached to any document; attach them
+                to the related document. Should only be set by Chatter.
+        """
+        ir_attachment = self.pool.get('ir.attachment')
+        mail_message = self.pool.get('mail.message')
+
+        # 1. Pre-processing: body, partner_ids, type and subtype
+        if content_subtype == 'plaintext':
+            body = tools.plaintext2html(body)
+
+        partner_ids = kwargs.pop('partner_ids', [])
+        if parent_id:
+            parent_message = self.pool.get('mail.message').browse(cr, uid, parent_id, context=context)
+            partner_ids += [(4, partner.id) for partner in parent_message.partner_ids]
+            # TDE FIXME HACK: mail.thread -> private message
+            if self._name == 'mail.thread' and parent_message.author_id.id:
+                partner_ids.append((4, parent_message.author_id.id))
+
+        message_type = kwargs.pop('type', 'comment')
+        message_subtype = kwargs.pop('subtype', 'mail.mt_comment')
+
+        # 2. Post message
+        new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=message_type,
+                        subtype=message_subtype, parent_id=parent_id, context=context, partner_ids=partner_ids, **kwargs)
+
+        # 3. Post-processing
+        # HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
+        if attachment_ids:
+            # TDE FIXME (?): when posting a private message, we use mail.thread as a model
+            # However, attaching doc to mail.thread is not possible, mail.thread does not have any table
+            model = self._name
+            if model == 'mail.thread':
+                model = False
+            filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
+                ('res_model', '=', 'mail.compose.message'),
+                ('res_id', '=', 0),
+                ('create_uid', '=', uid),
+                ('id', 'in', attachment_ids)], context=context)
+            if filtered_attachment_ids:
+                if thread_id and model:
+                    ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
+                mail_message.write(cr, SUPERUSER_ID, [new_message_id], {'attachment_ids': [(6, 0, [pid for pid in attachment_ids])]}, context=context)
+
+        return new_message_id
 
     #------------------------------------------------------
     # Followers API
     #------------------------------------------------------
 
-    def message_post_api(self, cr, uid, thread_id, body='', subject=False, type='notification',
-                        subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
-        # if the user write on his wall
-        if self._name=='res.partner' and (not thread_id or not thread_id[0]):
-            user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
-            thread_id = user.partner_id.id
-
-        added_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=type,
-                        subtype=subtype, parent_id=parent_id, attachments=attachments, context=context)
-        added_message = self.pool.get('mail.message').message_read(cr, uid, [added_message_id])
-        return added_message
-
-    def get_message_subtypes(self, cr, uid, ids, context=None):
-        """ message_subtype_data: data about document subtypes: which are
-                available, which are followed if any """
+    def message_get_subscription_data(self, cr, uid, ids, context=None):
+        """ Wrapper to get subtypes data. """
         return self._get_subscription_data(cr, uid, ids, None, None, context=context)
 
     def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
         """ Wrapper on message_subscribe, using users. If user_ids is not
             provided, subscribe uid instead. """
-        if not user_ids:
-            return False
+        if user_ids is None:
+            user_ids = [uid]
         partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
         return self.message_subscribe(cr, uid, ids, partner_ids, subtype_ids=subtype_ids, context=context)
 
@@ -728,14 +809,14 @@ class mail_thread(osv.AbstractModel):
             subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
         # update the subscriptions
         fol_obj = self.pool.get('mail.followers')
-        fol_ids = fol_obj.search(cr, 1, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context)
-        fol_obj.write(cr, 1, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
+        fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context)
+        fol_obj.write(cr, SUPERUSER_ID, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
         return True
 
     def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None):
         """ Wrapper on message_subscribe, using users. If user_ids is not
             provided, unsubscribe uid instead. """
-        if not user_ids:
+        if user_ids is None:
             user_ids = [uid]
         partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
         return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context)