[FIX] mail: read ir.config_parameter with SUPERUSER_ID
[odoo/odoo.git] / addons / mail / mail_message.py
index b040aee..a2d3828 100644 (file)
 ##############################################################################
 
 import logging
+import re
+
 from openerp import tools
 
 from email.header import decode_header
+from email.utils import formataddr
 from openerp import SUPERUSER_ID
 from openerp.osv import osv, orm, fields
 from openerp.tools import html_email_clean
 from openerp.tools.translate import _
+from HTMLParser import HTMLParser
 
 _logger = logging.getLogger(__name__)
 
@@ -43,6 +47,19 @@ def decode(text):
         text = decode_header(text.replace('\r', ''))
         return ''.join([tools.ustr(x[0], x[1]) for x in text])
 
+class MLStripper(HTMLParser):
+    def __init__(self):
+        self.reset()
+        self.fed = []
+    def handle_data(self, d):
+        self.fed.append(d)
+    def get_data(self):
+        return ''.join(self.fed)
+
+def strip_tags(html):
+    s = MLStripper()
+    s.feed(html)
+    return s.get_data()
 
 class mail_message(osv.Model):
     """ Messages model: system notification (replacing res.log notifications),
@@ -61,7 +78,8 @@ class mail_message(osv.Model):
 
     def default_get(self, cr, uid, fields, context=None):
         # protection for `default_type` values leaking from menu action context (e.g. for invoices)
-        if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection:
+        if context and context.get('default_type') and context.get('default_type') not in [
+                val[0] for val in self._columns['type'].selection]:
             context = dict(context, default_type=None)
         return super(mail_message, self).default_get(cr, uid, fields, context=context)
 
@@ -76,15 +94,15 @@ class mail_message(osv.Model):
         # TDE note: regroup by model/ids, to have less queries to perform
         result = dict.fromkeys(ids, False)
         for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
-            if not message.get('model') or not message.get('res_id') or not self.pool.get(message['model']):
+            if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
                 continue
-            result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
+            result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
         return result
 
     def _get_to_read(self, cr, uid, ids, name, arg, context=None):
         """ Compute if the message is unread by the current user. """
         res = dict((id, False) for id in ids)
-        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
         notif_obj = self.pool.get('mail.notification')
         notif_ids = notif_obj.search(cr, uid, [
             ('partner_id', 'in', [partner_id]),
@@ -103,7 +121,7 @@ class mail_message(osv.Model):
     def _get_starred(self, cr, uid, ids, name, arg, context=None):
         """ Compute if the message is unread by the current user. """
         res = dict((id, False) for id in ids)
-        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
         notif_obj = self.pool.get('mail.notification')
         notif_ids = notif_obj.search(cr, uid, [
             ('partner_id', 'in', [partner_id]),
@@ -125,7 +143,7 @@ class mail_message(osv.Model):
             ids = [ids]
         res = []
         for message in self.browse(cr, uid, ids, context=context):
-            name = '%s: %s' % (message.subject or '', message.body or '')
+            name = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '')
             res.append((message.id, self._shorten_name(name.lstrip(' :'))))
         return res
 
@@ -139,9 +157,12 @@ class mail_message(osv.Model):
                  "message, comment for other messages such as user replies"),
         'email_from': fields.char('From',
             help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
+        'reply_to': fields.char('Reply-To',
+            help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
         'author_id': fields.many2one('res.partner', 'Author', select=1,
             ondelete='set null',
             help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
+        'author_avatar': fields.related('author_id', 'image_small', type="binary", string="Author's Avatar"),
         'partner_ids': fields.many2many('res.partner', string='Recipients'),
         'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
             'message_id', 'partner_id', 'Notified partners',
@@ -174,19 +195,29 @@ class mail_message(osv.Model):
         'vote_user_ids': fields.many2many('res.users', 'mail_vote',
             'message_id', 'user_id', string='Votes',
             help='Users that voted for this message'),
+        'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
     }
 
     def _needaction_domain_get(self, cr, uid, context=None):
         return [('to_read', '=', True)]
 
+    def _get_default_from(self, cr, uid, context=None):
+        this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
+        if this.alias_name and this.alias_domain:
+            return formataddr((this.name, '%s@%s' % (this.alias_name, this.alias_domain)))
+        elif this.email:
+            return formataddr((this.name, this.email))
+        raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
+
     def _get_default_author(self, cr, uid, context=None):
-        return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
 
     _defaults = {
         'type': 'email',
-        'date': lambda *a: fields.datetime.now(),
-        'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+        'date': fields.datetime.now,
+        'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
         'body': '',
+        'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
     }
 
     #------------------------------------------------------
@@ -233,9 +264,11 @@ class mail_message(osv.Model):
             :param bool read: set notification as (un)read
             :param bool create_missing: create notifications for missing entries
                 (i.e. when acting on displayed messages not notified)
+
+            :return number of message mark as read
         """
         notification_obj = self.pool.get('mail.notification')
-        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
         domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
         if not create_missing:
             domain += [('read', '=', not read)]
@@ -243,14 +276,16 @@ class mail_message(osv.Model):
 
         # all message have notifications: already set them as (un)read
         if len(notif_ids) == len(msg_ids) or not create_missing:
-            return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+            notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+            return len(notif_ids)
 
         # some messages do not have notifications: find which one, create notification, update read status
         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
         for msg_id in to_create_msg_ids:
             notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
-        return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+        notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+        return len(notif_ids)
 
     def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
         """ Set messages as (un)starred. Technically, the notifications related
@@ -261,23 +296,29 @@ class mail_message(osv.Model):
                 (i.e. when acting on displayed messages not notified)
         """
         notification_obj = self.pool.get('mail.notification')
-        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
         domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
         if not create_missing:
             domain += [('starred', '=', not starred)]
+        values = {
+            'starred': starred
+        }
+        if starred:
+            values['read'] = False
+
         notif_ids = notification_obj.search(cr, uid, domain, context=context)
 
         # all message have notifications: already set them as (un)starred
         if len(notif_ids) == len(msg_ids) or not create_missing:
-            notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
+            notification_obj.write(cr, uid, notif_ids, values, context=context)
             return starred
 
         # some messages do not have notifications: find which one, create notification, update starred status
         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
         for msg_id in to_create_msg_ids:
-            notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context)
-        notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
+            notification_obj.create(cr, uid, dict(values, partner_id=user_pid, message_id=msg_id), context=context)
+        notification_obj.write(cr, uid, notif_ids, values, context=context)
         return starred
 
     #------------------------------------------------------
@@ -293,7 +334,7 @@ class mail_message(osv.Model):
         """
         res_partner_obj = self.pool.get('res.partner')
         ir_attachment_obj = self.pool.get('ir.attachment')
-        pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+        pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
 
         # 1. Aggregate partners (author_id and partner_ids) and attachments
         partner_ids = set()
@@ -301,7 +342,9 @@ class mail_message(osv.Model):
         for key, message in message_tree.iteritems():
             if message.author_id:
                 partner_ids |= set([message.author_id.id])
-            if message.partner_ids:
+            if message.subtype_id and message.notified_partner_ids:  # take notified people of message with a subtype
+                partner_ids |= set([partner.id for partner in message.notified_partner_ids])
+            elif not message.subtype_id and message.partner_ids:  # take specified people of message without a subtype (log)
                 partner_ids |= set([partner.id for partner in message.partner_ids])
             if message.attachment_ids:
                 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
@@ -310,8 +353,13 @@ class mail_message(osv.Model):
         partner_tree = dict((partner[0], partner) for partner in partners)
 
         # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
-        attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname'], context=context)
-        attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments)
+        attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type_icon'], context=context)
+        attachments_tree = dict((attachment['id'], {
+            'id': attachment['id'],
+            'filename': attachment['datas_fname'],
+            'name': attachment['name'],
+            'file_type_icon': attachment['file_type_icon'],
+        }) for attachment in attachments)
 
         # 3. Update message dictionaries
         for message_dict in messages:
@@ -322,9 +370,12 @@ class mail_message(osv.Model):
             else:
                 author = (0, message.email_from)
             partner_ids = []
-            for partner in message.partner_ids:
-                if partner.id in partner_tree:
-                    partner_ids.append(partner_tree[partner.id])
+            if message.subtype_id:
+                partner_ids = [partner_tree[partner.id] for partner in message.notified_partner_ids
+                                if partner.id in partner_tree]
+            else:
+                partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
+                                if partner.id in partner_tree]
             attachment_ids = []
             for attachment in message.attachment_ids:
                 if attachment.id in attachments_tree:
@@ -334,6 +385,7 @@ class mail_message(osv.Model):
                 'author_id': author,
                 'partner_ids': partner_ids,
                 'attachment_ids': attachment_ids,
+                'user_pid': pid
                 })
         return True
 
@@ -352,9 +404,22 @@ class mail_message(osv.Model):
         vote_nb = len(message.vote_user_ids)
         has_voted = uid in [user.id for user in message.vote_user_ids]
 
+        try:
+            if parent_id:
+                max_length = 300
+            else:
+                max_length = 100
+            body_short = html_email_clean(message.body, remove=False, shorten=True, max_length=max_length)
+
+        except Exception:
+            body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
+            _logger.exception(Exception)
+
         return {'id': message.id,
                 'type': message.type,
-                'body': html_email_clean(message.body or ''),
+                'subtype': message.subtype_id.name if message.subtype_id else False,
+                'body': message.body,
+                'body_short': body_short,
                 'model': message.model,
                 'res_id': message.res_id,
                 'record_name': message.record_name,
@@ -364,6 +429,7 @@ class mail_message(osv.Model):
                 'parent_id': parent_id,
                 'is_private': is_private,
                 'author_id': False,
+                'author_avatar': message.author_avatar,
                 'is_author': False,
                 'partner_ids': [],
                 'vote_nb': vote_nb,
@@ -494,7 +560,6 @@ class mail_message(osv.Model):
         message_unload_ids = message_unload_ids if message_unload_ids is not None else []
         if message_unload_ids:
             domain += [('id', 'not in', message_unload_ids)]
-        notification_obj = self.pool.get('mail.notification')
         limit = limit or self._message_read_limit
         message_tree = {}
         message_list = []
@@ -554,7 +619,7 @@ class mail_message(osv.Model):
 
     def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
         doc_ids = doc_dict.keys()
-        allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
+        allowed_doc_ids = self.pool[doc_model].search(cr, uid, [('id', 'in', doc_ids)], context=context)
         return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
 
     def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
@@ -590,7 +655,7 @@ class mail_message(osv.Model):
         elif not ids:
             return ids
 
-        pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
+        pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
         author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
         model_ids = {}
 
@@ -624,14 +689,14 @@ class mail_message(osv.Model):
                 - no model, no res_id, I create a private message OR
                 - pid in message_follower_ids if model, res_id OR
                 - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
-                - uid have write access on the related document if model, res_id, OR
+                - uid have write or create access on the related document if model, res_id, OR
                 - otherwise: raise
             - write: if
                 - author_id == pid, uid is the author, OR
-                - uid has write access on the related document if model, res_id
+                - uid has write or create access on the related document if model, res_id
                 - otherwise: raise
             - unlink: if
-                - uid has write access on the related document if model, res_id
+                - uid has write or create access on the related document if model, res_id
                 - otherwise: raise
         """
         def _generate_model_record_ids(msg_val, msg_ids=[]):
@@ -640,8 +705,9 @@ class mail_message(osv.Model):
             """
             model_record_ids = {}
             for id in msg_ids:
-                if msg_val[id]['model'] and msg_val[id]['res_id']:
-                    model_record_ids.setdefault(msg_val[id]['model'], dict()).setdefault(msg_val[id]['res_id'], set()).add(msg_val[id]['res_id'])
+                vals = msg_val.get(id, {})
+                if vals.get('model') and vals.get('res_id'):
+                    model_record_ids.setdefault(vals['model'], set()).add(vals['res_id'])
             return model_record_ids
 
         if uid == SUPERUSER_ID:
@@ -650,10 +716,10 @@ class mail_message(osv.Model):
             ids = [ids]
         not_obj = self.pool.get('mail.notification')
         fol_obj = self.pool.get('mail.followers')
-        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+        partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
 
         # Read mail_message.ids to have their values
-        message_values = dict.fromkeys(ids)
+        message_values = dict.fromkeys(ids, {})
         cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
         for id, rmod, rid, author_id, parent_id in cr.fetchall():
             message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
@@ -687,10 +753,10 @@ class mail_message(osv.Model):
             ], context=context)
             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
         elif operation == 'create':
-            for doc_model, doc_dict in model_record_ids.items():
+            for doc_model, doc_ids in model_record_ids.items():
                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
                     ('res_model', '=', doc_model),
-                    ('res_id', 'in', list(doc_dict.keys())),
+                    ('res_id', 'in', list(doc_ids)),
                     ('partner_id', '=', partner_id),
                     ], context=context)
                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
@@ -701,15 +767,13 @@ class mail_message(osv.Model):
         other_ids = other_ids.difference(set(notified_ids))
         model_record_ids = _generate_model_record_ids(message_values, other_ids)
         document_related_ids = []
-        for model, doc_dict in model_record_ids.items():
-            model_obj = self.pool.get(model)
-            mids = model_obj.exists(cr, uid, doc_dict.keys())
-            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)
+        for model, doc_ids in model_record_ids.items():
+            model_obj = self.pool[model]
+            mids = model_obj.exists(cr, uid, list(doc_ids))
+            if hasattr(model_obj, 'check_mail_message_access'):
+                model_obj.check_mail_message_access(cr, uid, mids, operation, context=context)
             else:
-                model_obj.check_access_rights(cr, uid, operation)
-                model_obj.check_access_rule(cr, uid, mids, operation, context=context)
+                self.pool['mail.thread'].check_mail_message_access(cr, uid, mids, operation, model_obj=model_obj, context=context)
             document_related_ids += [mid for mid, message in message_values.iteritems()
                 if message.get('model') == model and message.get('res_id') in mids]
 
@@ -721,16 +785,67 @@ class mail_message(osv.Model):
                             _('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 _get_reply_to(self, cr, uid, values, context=None):
+        """ Return a specific reply_to: alias of the document through message_get_reply_to
+            or take the email_from
+        """
+        email_reply_to = None
+
+        ir_config_parameter = self.pool.get("ir.config_parameter")
+        catchall_domain = ir_config_parameter.get_param(cr, SUPERUSER_ID, "mail.catchall.domain", context=context)
+
+        # model, res_id, email_from: comes from values OR related message
+        model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
+
+        # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
+        if not email_reply_to and model and res_id and catchall_domain and hasattr(self.pool[model], 'message_get_reply_to'):
+            email_reply_to = self.pool[model].message_get_reply_to(cr, uid, [res_id], context=context)[0]
+        # no alias reply_to -> catchall alias
+        if not email_reply_to and catchall_domain:
+            catchall_alias = ir_config_parameter.get_param(cr, SUPERUSER_ID, "mail.catchall.alias", context=context)
+            if catchall_alias:
+                email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
+        # still no reply_to -> reply_to will be the email_from
+        if not email_reply_to and email_from:
+            email_reply_to = email_from
+
+        # format 'Document name <email_address>'
+        if email_reply_to and model and res_id:
+            emails = tools.email_split(email_reply_to)
+            if emails:
+                email_reply_to = emails[0]
+            document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
+            if document_name:
+                # generate reply to
+                email_reply_to = formataddr((_('Followers of %s') % document_name[1], email_reply_to))
+
+        return email_reply_to
+
+    def _get_message_id(self, cr, uid, values, context=None):
+        if values.get('reply_to'):
+            message_id = tools.generate_tracking_message_id('reply_to')
+        elif values.get('res_id') and values.get('model'):
+            message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
+        else:
+            message_id = tools.generate_tracking_message_id('private')
+        return message_id
+
     def create(self, cr, uid, values, context=None):
         if context is None:
             context = {}
         default_starred = context.pop('default_starred', False)
-        if not values.get('message_id') and values.get('res_id') and values.get('model'):
-            values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
-        elif not values.get('message_id'):
-            values['message_id'] = tools.generate_tracking_message_id('private')
+
+        if 'email_from' not in values:  # needed to compute reply_to
+            values['email_from'] = self._get_default_from(cr, uid, context=context)
+        if 'message_id' not in values:
+            values['message_id'] = self._get_message_id(cr, uid, values, context=context)
+        if 'reply_to' not in values:
+            values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
+
         newid = super(mail_message, self).create(cr, uid, values, context)
-        self._notify(cr, uid, newid, context=context)
+        self._notify(cr, uid, newid, context=context,
+                     force_send=context.get('mail_notify_force_send', True),
+                     user_signature=context.get('mail_notify_user_signature', True))
         # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
         # Because starred will call set_message_starred, that looks for notifications.
         # When creating a new mail_message, it will create a notification to a message
@@ -755,7 +870,7 @@ class mail_message(osv.Model):
         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:
+                if attach.res_model == self._name and (attach.res_id == message.id or attach.res_id == 0):
                     attachments_to_delete.append(attach.id)
         if attachments_to_delete:
             self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
@@ -844,40 +959,42 @@ class mail_message(osv.Model):
             return ''
         return result
 
-    def _notify(self, cr, uid, newid, context=None):
+    def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
         """ 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
         """
         notification_obj = self.pool.get('mail.notification')
         message = self.browse(cr, uid, newid, context=context)
-
         partners_to_notify = set([])
-        # message has no subtype_id: pure log message -> no partners, no one notified
-        if not message.subtype_id:
-            return True
-        # all partner_ids of the mail.message have to be notified
-        if message.partner_ids:
-            partners_to_notify |= set(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:
+
+        # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message)
+        if message.subtype_id and message.model and message.res_id:
             fol_obj = self.pool.get("mail.followers")
             # browse as SUPERUSER because rules could restrict the search results
-            fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
-                ('res_model', '=', message.model),
-                ('res_id', '=', message.res_id),
-                ('subtype_ids', 'in', message.subtype_id.id)
+            fol_ids = fol_obj.search(
+                cr, SUPERUSER_ID, [
+                    ('res_model', '=', message.model),
+                    ('res_id', '=', message.res_id),
                 ], context=context)
-            partners_to_notify |= set(fo.partner_id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
+            partners_to_notify |= set(
+                fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
+                if message.subtype_id.id in [st.id for st in fo.subtype_ids]
+            )
         # remove me from notified partners, unless the message is written on my own wall
-        if message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
-            partners_to_notify |= set([message.author_id])
+        if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
+            partners_to_notify |= set([message.author_id.id])
         elif message.author_id:
-            partners_to_notify = partners_to_notify - set([message.author_id])
+            partners_to_notify -= set([message.author_id.id])
+
+        # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
+        if message.partner_ids:
+            partners_to_notify |= set([p.id for p in message.partner_ids])
 
         # notify
-        if partners_to_notify:
-            self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
-        notification_obj._notify(cr, uid, newid, context=context)
+        notification_obj._notify(
+            cr, uid, newid, partners_to_notify=list(partners_to_notify), context=context,
+            force_send=force_send, user_signature=user_signature
+        )
         message.refresh()
 
         # An error appear when a user receive a notification without notifying