[REF] mail_message: message_read: re-implementation using read instead of browse...
[odoo/odoo.git] / addons / mail / mail_message.py
index 6c47e00..7a0675f 100644 (file)
 ##############################################################################
 
 import logging
-from email.header import decode_header
-from osv import osv, fields
+import openerp
 import tools
 
+from email.header import decode_header
+from openerp import SUPERUSER_ID
+from operator import itemgetter
+from osv import osv, orm, fields
+from tools.translate import _
+
 _logger = logging.getLogger(__name__)
 
 """ Some tools for parsing / creating email fields """
@@ -33,12 +38,10 @@ def decode(text):
         text = decode_header(text.replace('\r', ''))
         return ''.join([tools.ustr(x[0], x[1]) for x in text])
 
-class mail_message(osv.Model):
-    """Model holding messages: system notification (replacing res.log
-    notifications), comments (for OpenChatter feature). This model also
-    provides facilities to parse new email messages. Type of messages are
-    differentiated using the 'type' column. """
 
+class mail_message(osv.Model):
+    """ Messages model: system notification (replacing res.log notifications),
+        comments (OpenChatter discussion) and incoming emails. """
     _name = 'mail.message'
     _description = 'Message'
     _inherit = ['ir.needaction_mixin']
@@ -48,23 +51,26 @@ class mail_message(osv.Model):
     _message_record_name_length = 18
 
     def _shorten_name(self, name):
-        if len(name) <= (self._message_record_name_length+3):
+        if len(name) <= (self._message_record_name_length + 3):
             return name
         return name[:self._message_record_name_length] + '...'
 
     def _get_record_name(self, cr, uid, ids, name, arg, context=None):
         """ Return the related document name, using get_name. """
-        result = dict.fromkeys(ids, '')
-        for message in self.browse(cr, uid, ids, context=context):
+        result = dict.fromkeys(ids, False)
+        for message in self.browse(cr, 1, ids, context=context):
             if not message.model or not message.res_id:
                 continue
-            result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
+            try:
+                result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
+            except (orm.except_orm, osv.except_osv):
+                pass
         return result
 
     def _get_unread(self, cr, uid, ids, name, arg, context=None):
         """ Compute if the message is unread by the current user. """
         res = dict((id, {'unread': False}) for id in ids)
-        partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
         notif_obj = self.pool.get('mail.notification')
         notif_ids = notif_obj.search(cr, uid, [
             ('partner_id', 'in', [partner_id]),
@@ -76,20 +82,17 @@ class mail_message(osv.Model):
         return res
 
     def _search_unread(self, cr, uid, obj, name, domain, context=None):
-        """ Search for messages unread by the current user. """
-        read_value = not domain[0][2]
-        partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
-        cr.execute("""  SELECT mail_message.id \
-                        FROM mail_message \
-                        JOIN mail_notification ON ( \
-                            mail_notification.message_id = mail_message.id ) \
-                        WHERE mail_notification.partner_id = %s AND \
-                            mail_notification.read = %s \
-                    """ % (partner_id, read_value) )
-        res = cr.fetchall()
-        message_ids = [r[0] for r in res]
-        return [('id', 'in', message_ids)]
-
+        """ Search for messages unread by the current user. Condition is
+            inversed because we search unread message on a read column. """
+        if domain[0][2]:
+            read_cond = '(read = false or read is null)'
+        else:
+            read_cond = 'read = true'
+        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        cr.execute("SELECT message_id FROM mail_notification "\
+                        "WHERE partner_id = %%s AND %s" % read_cond,
+                    (partner_id,))
+        return [('id', 'in', [r[0] for r in cr.fetchall()])]
 
     def name_get(self, cr, uid, ids, context=None):
         # name_get may receive int id instead of an id list
@@ -102,9 +105,8 @@ class mail_message(osv.Model):
         return res
 
     _columns = {
-        # should we keep a distinction between email and comment ?
         'type': fields.selection([
-                        ('email', 'email'),
+                        ('email', 'Email'),
                         ('comment', 'Comment'),
                         ('notification', 'System notification'),
                         ], 'Type',
@@ -129,6 +131,10 @@ class mail_message(osv.Model):
         'unread': fields.function(_get_unread, fnct_search=_search_unread,
             type='boolean', string='Unread',
             help='Functional field to search for unread messages linked to uid'),
+        'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
+        'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes',
+            help='Users that voted for this message'),
+        'is_private': fields.boolean('Private message'),
     }
 
     def _needaction_domain_get(self, cr, uid, context=None):
@@ -137,134 +143,229 @@ class mail_message(osv.Model):
         return []
 
     def _get_default_author(self, cr, uid, context=None):
-        return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+        # remove context to avoid possible hack in browse with superadmin using context keys that could trigger a specific behavior
+        return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
 
     _defaults = {
         'type': 'email',
         'date': lambda *a: fields.datetime.now(),
-        'author_id': _get_default_author
+        'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+        'body': '',
+        'is_private': True,
     }
 
+    #------------------------------------------------------
+    # Vote/Like
+    #------------------------------------------------------
+
+    def vote_toggle(self, cr, uid, ids, user_ids=None, context=None):
+        ''' Toggles voting. Done as SUPERUSER_ID because of write access on
+            mail.message not always granted. '''
+        if not user_ids:
+            user_ids = [uid]
+        for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
+            for user_id in user_ids:
+                has_voted = user_id in message.get('vote_user_ids')
+                if not has_voted:
+                    self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, user_id)]}, context=context)
+                else:
+                    self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, user_id)]}, context=context)
+        return not(has_voted) or False
 
     #------------------------------------------------------
     # Message loading for web interface
     #------------------------------------------------------
 
-    def _message_dict_get(self, cr, uid, msg, context=None):
-        """ Return a dict representation of the message browse record. """
-        attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
-        author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
-        author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
-        partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
+    def _message_get_dict(self, cr, uid, message, context=None):
+        """ Return a dict representation of the message.
+
+            :param dict message: read result of a mail.message
+        """
+        if uid in message['vote_user_ids']:
+            has_voted = True
+        else:
+            has_voted = False
+
+        try:
+            attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, [message['attachment_ids']], context=context)]
+        except (orm.except_orm, osv.except_osv):
+            attachment_ids = []
+
+        try:
+            partner_ids = self.pool.get('res.partner').name_get(cr, uid, [message['partner_ids']], context=context)
+        except (orm.except_orm, osv.except_osv):
+            partner_ids = []
+
         return {
-            'id': msg.id,
-            'type': msg.type,
+            'id': message['id'],
+            'type': message['type'],
             'attachment_ids': attachment_ids,
-            'body': msg.body,
-            'model': msg.model,
-            'res_id': msg.res_id,
-            'record_name': msg.record_name,
-            'subject': msg.subject,
-            'date': msg.date,
-            'author_id': author_id,
-            'author_user_id': author_user_id,
+            'body': message['body'],
+            'model': message['model'],
+            'res_id': message['res_id'],
+            'record_name': message['record_name'],
+            'subject': message['subject'],
+            'date': message['date'],
+            'author_id': message['author_id'],
+            'is_author': message['author_id'] and message['author_id'][0] == uid,
             'partner_ids': partner_ids,
-            'child_ids': [],
+            'parent_id': message['parent_id'] and message['parent_id'][0] or False,
+            # 'vote_user_ids': vote_ids,
+            'has_voted': has_voted,
+            # 'unread': msg.unread and msg.unread['unread'] or False
         }
 
-    def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
-        """ Given a tree with several roots of following structure :
-            [
-                {'id': 1, 'child_ids':[
-                    {'id': 11, 'child_ids': [...] },
-                ] },
-                {...}
-            ]
-            Flatten it to have a maximum number of level, with 0 being
-            completely flat.
-            Perform the flattening at leafs if above the maximum depth, then get
-            back in the tree.
-        """
-        def _flatten(msg_dict):
-            """ from    {'id': x, 'child_ids': [{child1}, {child2}]}
-                get     [{'id': x, 'child_ids': []}, {child1}, {child2}]
-            """
-            child_ids = msg_dict.pop('child_ids', [])
-            msg_dict['child_ids'] = []
-            return [msg_dict] + child_ids
-        # Depth-first flattening
-        for message in messages:
-            message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level+1, level, context=context)
-        # Flatten if above maximum depth
-        if current_level < level:
-            return messages
-        new_list = []
-        for x in range(0, len(messages)):
-            flatenned = _flatten(messages[x])
-            for flat in flatenned:
-                new_list.append(flat)
-        messages = new_list
-        return messages
-
-    def _debug_print_tree(self, tree, prefix=''):
-        for elem in tree:
-            print '%s%s (%s childs: %s)' % (prefix, elem['id'], len(elem['child_ids']), [xelem['id'] for xelem in elem['child_ids']])
-            if elem['child_ids']:
-                self._debug_print_tree(elem['child_ids'], prefix+'--')
-
-    def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
-        """ 
-            If IDS are provided, fetch these records, otherwise use the domain to
-            fetch the matching records. After having fetched the records provided
-            by IDS, it will fetch children (according to thread_level).
-            
-            Return [
-            
-            ]
+    def _message_read_expandable(self, cr, uid, tree, result, message_loaded, domain, context, parent_id, limit):
+        """ Create the expandable message for all parent message read
+            this function is used by message_read
+            TDE note: place use default values for args, and comment your vars !!
+
+            :param dict tree: tree of message ids
         """
-        limit = limit or self._message_read_limit
-        context = context or {}
-        if ids is False:
-            ids = self.search(cr, uid, domain, context=context, limit=limit)
-
-        # FP Todo: flatten to max X level of mail_thread
-        messages = self.browse(cr, uid, ids, context=context)
-
-        result = []
-        tree = {} # key: ID, value: record
-        for msg in messages:
-            if len(result)<(limit-1):
-                record = self._message_dict_get(cr, uid, msg, context=context)
-                if thread_level and msg.parent_id:
-                    while msg.parent_id:
-                        if msg.parent_id.id in tree:
-                            record_parent = tree[msg.parent_id.id]
-                        else:
-                            record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
-                            if msg.parent_id.parent_id:
-                                tree[msg.parent_id.id] = record_parent
-                        if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
-                            record_parent['child_ids'].append(record)
-                        record = record_parent
-                        msg = msg.parent_id
-                if msg.id not in tree:
-                    result.append(record)
-                    tree[msg.id] = record
-            else:
+        tree_not = []
+        # expandable for not show message
+        for msg_id in tree:
+            # get all childs
+            not_loaded_ids = self.search(cr, SUPERUSER_ID, [
+                ('parent_id', '=', msg_id),
+                ('id', 'not in', message_loaded)
+                ], context=context, limit=1000)
+            # group childs not read
+            id_min = None
+            id_max = None
+            nb = 0
+            for not_loaded_id in not_loaded_ids:
+                if not_loaded_id not in tree:
+                    nb += 1
+                    if id_min == None or id_min > not_loaded_id:
+                        id_min = not_loaded_id
+                    if id_max == None or id_max < not_loaded_id:
+                        id_max = not_loaded_id
+                    tree_not.append(not_loaded_id)
+                else:
+                    if nb > 0:
+                        result.append({
+                            'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', msg_id)],
+                            'nb_messages': nb,
+                            'type': 'expandable',
+                            'parent_id': msg_id,
+                            'id':  id_min,
+                        })
+                    id_min = None
+                    id_max = None
+                    nb = 0
+            if nb > 0:
                 result.append({
+                    'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', msg_id)],
+                    'nb_messages': nb,
                     'type': 'expandable',
-                    'domain': [('id','<=', msg.id)]+domain,
-                    'context': context,
-                    'thread_level': thread_level  # should be improve accodting to level of records
+                    'parent_id': msg_id,
+                    'id':  id_min
                 })
-                break
 
-        # Flatten the result
-        if thread_level > 0:
-            result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
+        # expandable for limit max
+        ids = self.search(cr, SUPERUSER_ID, domain + [('id', 'not in', message_loaded + tree + tree_not)], context=context, limit=1)
+        if len(ids) > 0:
+            result.append({
+                'domain': domain,
+                'nb_messages': 0,
+                'type': 'expandable',
+                'parent_id': parent_id,
+                'id': -1
+            })
+
+        result = sorted(result, key=lambda k: k['id'])
 
         return result
 
+    _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
+
+    def _get_parent(self, cr, uid, message, context=None):
+        """ Tools method that try to get the parent of a mail.message. If
+            no parent, or if uid has no access right on the parent, False
+            is returned.
+
+            :param dict message: read result of a mail.message
+        """
+        if not message['parent_id']:
+            return False
+        parent_id = message['parent_id'][0]
+        try:
+            return self.read(cr, uid, parent_id, self._message_read_fields, context=context)
+        except (orm.except_orm, osv.except_osv):
+            return False
+
+    def message_read(self, cr, uid, ids=False, domain=[], context=None, parent_id=False, limit=None):
+        """ Read messages from mail.message, and get back a structured tree
+            of messages to be displayed as discussion threads. If IDs is set,
+            fetch these records. Otherwise use the domain to fetch messages.
+            After having fetch messages, their parents will be added to obtain
+            well formed threads.
+
+            :param domain: optional domain for searching ids
+            :param limit: number of messages to fetch
+            :param parent_id: if parent_id reached, stop searching for
+                further parents
+            :return list: list of trees of messages
+        """
+        # don't read the message display by .js, in context message_loaded list
+        # TDE note: use an argument, do not use context
+        if context is None:
+            context = {}
+        if context.get('message_loaded'):
+            domain += [('id', 'not in', context.get('message_loaded'))]
+        limit = limit or self._message_read_limit
+        # tree = []
+        # result = []
+        record = None
+
+        id_tree = []
+        message_list = []
+
+        # select ids
+        # TDE note: should not receive [None] !!
+        if ids and ids != [None]:
+            for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
+                message_list.append(self._message_dict_get(cr, uid, message, context=context))
+            return message_list
+
+        # key: ID, value: record
+        ids = self.search(cr, SUPERUSER_ID, domain, context=context, limit=limit)
+
+        for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
+            # if not in record and not in message_loded list
+            if message['id'] not in id_tree and message['id'] not in context.get('message_loaded', []):
+                record = self._message_get_dict(cr, uid, message, context=context)
+                id_tree.append(message['id'])
+                message_list.append(record)
+
+            parent = self._get_parent(cr, uid, message, context=context)
+            while parent and parent['id'] != parent_id:
+                parent = message.parent_id.id
+                if parent['id'] not in id_tree:
+                    message = parent
+                    id_tree.append(message['id'])
+                    # if not in record and not in message_loded list
+                    if message['id'] not in context.get('message_loaded', []):
+                        record = self._message_get_dict(cr, uid, message, context=context)
+                        message_list.append(record)
+
+        message_list = sorted(message_list, key=lambda k: k['id'])
+
+        message_list = self._message_read_expandable(cr, uid, id_tree, message_list, context.get('message_loaded', []), domain, context, parent_id, limit)
+
+        return message_list
+
+    # TDE Note: do we need this ?
+    # def user_free_attachment(self, cr, uid, context=None):
+    #     attachment_list = []
+
+    #     attachment = self.pool.get('ir.attachment')
+    #     attachment_ids = attachment.search(cr, uid, [('res_model','=',''),('create_uid','=',uid)])
+    #     if len(attachment_ids):
+    #         attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)]
+
+    #     return attachment_list
 
     #------------------------------------------------------
     # Email api
@@ -275,108 +376,192 @@ class mail_message(osv.Model):
         if not cr.fetchone():
             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
 
-    def check(self, cr, uid, ids, mode, context=None):
+    def check_access_rule(self, cr, uid, ids, operation, context=None):
+        """ Access rules of mail.message:
+            - read: if
+                - notification exist (I receive pushed message) OR
+                - author_id = pid (I am the author) OR
+                - I can read the related document if res_model, res_id
+                - Otherwise: raise
+            - create: if
+                - I am in the document message_follower_ids OR
+                - I can write on the related document if res_model, res_id
+                - Otherwise: raise
+            - write: if
+                - I can write on the related document if res_model, res_id
+                - Otherwise: raise
+            - unlink: if
+                - I can write on the related document if res_model, res_id
+                - Otherwise: raise
         """
-        You can read/write a message if:
-          - you received it (a notification exists) or
-          - you can read the related document (res_model, res_id)
-        If a message is not attached to a document, normal access rights on
-        the mail.message object apply.
-        """
-        if not ids:
+        if uid == SUPERUSER_ID:
             return
         if isinstance(ids, (int, long)):
             ids = [ids]
-
-        partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
-
-        # check messages for which you have a notification
-        not_obj = self.pool.get('mail.notification')
-        not_ids = not_obj.search(cr, uid, [
-            ('partner_id', '=', partner_id),
-            ('message_id', 'in', ids),
-        ], context=context)
-        for notification in not_obj.browse(cr, uid, not_ids, context=context):
-            if notification.message_id.id in ids:
-                pass
-                # FP Note: we should put this again !!!
-                #ids.remove(notification.message_id.id)
-
-        # check messages according to related documents
-        res_ids = {}
-        cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
-        for rmod, rid in cr.fetchall():
-            if not (rmod and rid):
-                continue
-            res_ids.setdefault(rmod,set()).add(rid)
-
-        ima_obj = self.pool.get('ir.model.access')
-        for model, mids in res_ids.items():
-            mids = self.pool.get(model).exists(cr, uid, mids)
-            ima_obj.check(cr, uid, model, mode)
-            self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
+        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+
+        # Read mail_message.ids to have their values
+        model_record_ids = {}
+        message_values = dict.fromkeys(ids)
+        cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
+        for id, rmod, rid, author_id in cr.fetchall():
+            message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
+            if rmod:
+                model_record_ids.setdefault(rmod, set()).add(rid)
+
+        # Read: Check for received notifications -> could become an ir.rule, but not till we do not have a many2one variable field
+        if operation == 'read':
+            not_obj = self.pool.get('mail.notification')
+            not_ids = not_obj.search(cr, SUPERUSER_ID, [
+                ('partner_id', '=', partner_id),
+                ('message_id', 'in', ids),
+            ], context=context)
+            notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
+        else:
+            notified_ids = []
+        # Read: Check messages you are author -> could become an ir.rule, but not till we do not have a many2one variable field
+        if operation == 'read':
+            author_ids = [mid for mid, message in message_values.iteritems()
+                if message.get('author_id') and message.get('author_id') == partner_id]
+        else:
+            author_ids = []
+
+        # Create: Check message_follower_ids
+        if operation == 'create':
+            doc_follower_ids = []
+            for model, mids in model_record_ids.items():
+                fol_obj = self.pool.get('mail.followers')
+                fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
+                    ('res_model', '=', model),
+                    ('res_id', 'in', list(mids)),
+                    ('partner_id', '=', partner_id),
+                    ], context=context)
+                fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
+                doc_follower_ids += [mid for mid, message in message_values.iteritems()
+                    if message.get('res_model') == model and message.get('res_id') in fol_mids]
+        else:
+            doc_follower_ids = []
+
+        # Calculate remaining ids, and related model/res_ids
+        model_record_ids = {}
+        other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids))
+        for id in other_ids:
+            if message_values[id]['res_model']:
+                model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
+
+        # CRUD: Access rights related to the document
+        document_related_ids = []
+        for model, mids in model_record_ids.items():
+            model_obj = self.pool.get(model)
+            mids = model_obj.exists(cr, uid, mids)
+            if operation in ['create', 'write', 'unlink']:
+                model_obj.check_access_rights(cr, uid, 'write')
+                model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
+            else:
+                model_obj.check_access_rights(cr, uid, operation)
+                model_obj.check_access_rule(cr, uid, mids, operation, context=context)
+            document_related_ids += [mid for mid, message in message_values.iteritems()
+                if message.get('res_model') == model and message.get('res_id') in mids]
+
+        # Calculate remaining ids: if not void, raise an error
+        other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids), set(document_related_ids))
+        if not other_ids:
+            return
+        raise orm.except_orm(_('Access Denied'),
+                            _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
+                            (self._description, operation))
 
     def create(self, cr, uid, values, context=None):
         if not values.get('message_id') and values.get('res_id') and values.get('model'):
-            values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s'% values)
+            values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
         newid = super(mail_message, self).create(cr, uid, values, context)
-        self.check(cr, uid, [newid], mode='create', context=context)
-        self.notify(cr, uid, newid, context=context)
+        self._notify(cr, SUPERUSER_ID, newid, context=context)
         return newid
 
-    def notify(self, cr, uid, newid, context=None):
+    def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
+        """ Override to explicitely call check_access_rule, that is not called
+            by the ORM. It instead directly fetches ir.rules and apply them. """
+        res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
+        self.check_access_rule(cr, uid, ids, 'read', context=context)
+        return res
+
+    def unlink(self, cr, uid, ids, context=None):
+        # cascade-delete attachments that are directly attached to the message (should only happen
+        # for mail.messages that act as parent for a standalone mail.mail record).
+        attachments_to_delete = []
+        for message in self.browse(cr, uid, ids, context=context):
+            for attach in message.attachment_ids:
+                if attach.res_model == self._name and attach.res_id == message.id:
+                    attachments_to_delete.append(attach.id)
+        if attachments_to_delete:
+            self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
+        return super(mail_message, self).unlink(cr, uid, ids, context=context)
+
+    def _notify_followers(self, cr, uid, newid, message, context=None):
         """ Add the related record followers to the destination partner_ids.
-            Call mail_notification.notify to manage the email sending
         """
-        message = self.browse(cr, uid, newid, context=context)
-        partners_to_notify = []
-        # add all partner_ids of the message
+        partners_to_notify = set([])
+        # message has no subtype_id: pure log message -> no partners, no one notified
+        if not message.subtype_id:
+            message.write({'partner_ids': [5]})
+            return True
+        # all partner_ids of the mail.message have to be notified
         if message.partner_ids:
-            for partner in message.partner_ids:
-                if partner.id not in partners_to_notify:
-                    partners_to_notify.append(partner.id)
-        # add all followers and set them as partner_ids
+            partners_to_notify |= set(partner.id for partner in message.partner_ids)
+        # all followers of the mail.message document have to be added as partners and notified
         if message.model and message.res_id:
-            modobj = self.pool.get(message.model)
-            for follower in modobj.browse(cr, uid, message.res_id, context=context).message_follower_ids:
-                partners_to_notify.append(follower.id)
-                self.write(cr, uid, [newid], {'partner_ids': [(4, follower.id)]}, context=context)
-        self.pool.get('mail.notification').notify(cr, uid, partners_to_notify, newid, context=context)
-
-    def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
-        self.check(cr, uid, ids, 'read', context=context)
-        return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
+            fol_obj = self.pool.get("mail.followers")
+            fol_ids = fol_obj.search(cr, uid, [('res_model', '=', message.model), ('res_id', '=', message.res_id), ('subtype_ids', 'in', message.subtype_id.id)], context=context)
+            fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context)
+            extra_notified = set(fol.partner_id.id for fol in fol_objs)
+            missing_notified = extra_notified - partners_to_notify
+            missing_notified = missing_notified
+            if missing_notified:
+                self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context)
+            partners_to_notify |= extra_notified
+
+    def _notify(self, cr, uid, newid, context=None):
+        """ Add the related record followers to the destination partner_ids if is not a private message.
+            Call mail_notification.notify to manage the email sending
+        """
+        message = self.browse(cr, uid, newid, context=context)
+        if message and (message.is_private!=False and message.is_private!=None):
+            self._notify_followers(cr, uid, newid, message, context=context)
+        
+        # add myself if I wrote on my wall, 
+        # unless remove myself author
+        if ((message.model=="res.partner" and message.res_id==message.author_id.id)):
+            self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, message.author_id.id)]}, context=context)
+        else:
+            self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, message.author_id.id)]}, context=context)
+
+        self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
 
     def copy(self, cr, uid, id, default=None, context=None):
         """Overridden to avoid duplicating fields that are unique to each email"""
         if default is None:
             default = {}
-        self.check(cr, uid, [id], 'read', context=context)
         default.update(message_id=False, headers=False)
-        return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
-
-    def write(self, cr, uid, ids, vals, context=None):
-        result = super(mail_message, self).write(cr, uid, ids, vals, context)
-        self.check(cr, uid, ids, 'write', context=context)
-        return result
-
-    def unlink(self, cr, uid, ids, context=None):
-        self.check(cr, uid, ids, 'unlink', context=context)
-        return super(mail_message, self).unlink(cr, uid, ids, context)
-
-
-class mail_notification(osv.Model):
-    """ mail_notification is a relational table modeling messages pushed to partners.
-    """
-    _inherit = 'mail.notification'
-    _columns = {
-        'message_id': fields.many2one('mail.message', string='Message',
-                        ondelete='cascade', required=True, select=1),
-    }
-
-    def set_message_read(self, cr, uid, msg_id, context=None):
-        partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
-        notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
-        return self.write(cr, uid, notif_ids, {'read': True}, context=context)
+        return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
 
+    #------------------------------------------------------
+    # Tools
+    #------------------------------------------------------
 
+    def check_partners_email(self, cr, uid, partner_ids, context=None):
+        """ Verify that selected partner_ids have an email_address defined.
+            Otherwise throw a warning. """
+        partner_wo_email_lst = []
+        for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+            if not partner.email:
+                partner_wo_email_lst.append(partner)
+        if not partner_wo_email_lst:
+            return {}
+        warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
+        for partner in partner_wo_email_lst:
+            warning_msg += '\n- %s' % (partner.name)
+        return {'warning': {
+                    'title': _('Partners email addresses not found'),
+                    'message': warning_msg,
+                    }
+                }