[FIX] mail_thread: fixed duplicate emails when replying to an incoming email.
[odoo/odoo.git] / addons / mail / mail_thread.py
index 71ef8fa..46052b0 100644 (file)
@@ -25,6 +25,7 @@ import dateutil
 import email
 import logging
 import pytz
+import re
 import time
 import xmlrpclib
 from email.message import Message
@@ -39,7 +40,7 @@ _logger = logging.getLogger(__name__)
 
 
 def decode_header(message, header, separator=' '):
-    return separator.join(map(decode, message.get_all(header, [])))
+    return separator.join(map(decode, filter(None, message.get_all(header, []))))
 
 
 class mail_thread(osv.AbstractModel):
@@ -242,7 +243,7 @@ class mail_thread(osv.AbstractModel):
         # subscribe uid unless asked not to
         if not context.get('mail_create_nosubscribe'):
             self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
-            self.message_subscribe_from_parent(cr, uid, [thread_id], values.keys(), context=context)
+        self.message_auto_subscribe(cr, uid, [thread_id], values.keys(), context=context)
 
         # automatic logging unless asked not to (mainly for various testing purpose)
         if not context.get('mail_create_nolog'):
@@ -260,7 +261,7 @@ class mail_thread(osv.AbstractModel):
 
         # Perform write, update followers
         result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
-        self.message_subscribe_from_parent(cr, uid, ids, values.keys(), context=context)
+        self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context)
 
         # Perform the tracking
         if tracked_fields:
@@ -388,6 +389,18 @@ class mail_thread(osv.AbstractModel):
         return []
 
     #------------------------------------------------------
+    # Email specific
+    #------------------------------------------------------
+
+    def message_get_reply_to(self, cr, uid, ids, context=None):
+        if not self._inherits.get('mail.alias'):
+            return [False for id in ids]
+        return ["%s@%s" % (record['alias_name'], record['alias_domain'])
+                    if record.get('alias_domain') and record.get('alias_name')
+                    else False
+                    for record in self.read(cr, uid, ids, ['alias_name', 'alias_domain'], context=context)]
+
+    #------------------------------------------------------
     # Mail gateway
     #------------------------------------------------------
 
@@ -491,7 +504,13 @@ class mail_thread(osv.AbstractModel):
                 for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
                     user_id = alias.alias_user_id.id
                     if not user_id:
-                        user_id = self._message_find_user_id(cr, uid, message, context=context)
+                        # TDE note: this could cause crashes, because no clue that the user
+                        # that send the email has the right to create or modify a new document
+                        # Fallback on user_id = uid
+                        # Note: recognized partners will be added as followers anyway
+                        # user_id = self._message_find_user_id(cr, uid, message, context=context)
+                        user_id = uid
+                        _logger.debug('No matching user_id for the alias %s', alias.alias_name)
                     routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
                                    eval(alias.alias_defaults), user_id))
                 _logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
@@ -629,14 +648,14 @@ class mail_thread(osv.AbstractModel):
         """
         if context is None:
             context = {}
+        data = {}
+        if isinstance(custom_values, dict):
+            data = custom_values.copy()
         model = context.get('thread_model') or self._name
         model_pool = self.pool.get(model)
         fields = model_pool.fields_get(cr, uid, context=context)
-        data = model_pool.default_get(cr, uid, fields, context=context)
         if 'name' in fields and not data.get('name'):
             data['name'] = msg_dict.get('subject', '')
-        if custom_values and isinstance(custom_values, dict):
-            data.update(custom_values)
         res_id = model_pool.create(cr, uid, data, context=context)
         return res_id
 
@@ -754,7 +773,7 @@ class mail_thread(osv.AbstractModel):
                 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)
+        partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
         msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
 
         if 'Date' in message:
@@ -801,6 +820,42 @@ class mail_thread(osv.AbstractModel):
                         "now deprecated res.log.")
         self.message_post(cr, uid, [id], message, context=context)
 
+    def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+        """ Convert a list of emails into a list partner_ids and a list
+            new_partner_ids. The return value is non conventional because
+            it is meant to be used by the mail widget.
+
+            :return dict: partner_ids and new_partner_ids
+        """
+        partner_obj = self.pool.get('res.partner')
+        mail_message_obj = self.pool.get('mail.message')
+
+        partner_ids = []
+        new_partner_ids = []
+        for email in emails:
+            m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
+            name = m.group(2) or m.group(0)
+            email = m.group(3)
+            ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context)
+            if ids:
+                partner_ids.append(ids[0])
+                partner_id = ids[0]
+            else:
+                partner_id = partner_obj.create(cr, uid, {
+                        'name': name or email,
+                        'email': email,
+                    }, context=context)
+                new_partner_ids.append(partner_id)
+
+            # link mail with this from mail to the new partner id
+            message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
+            if message_ids:
+                mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context)
+        return {
+            'partner_ids': partner_ids,
+            'new_partner_ids': new_partner_ids,
+        }
+
     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
                         subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
         """ Post a new message in an existing thread, returning the new
@@ -889,7 +944,7 @@ class mail_thread(osv.AbstractModel):
         return mail_message.create(cr, uid, values, context=context)
 
     def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
-                                attachment_ids=None, extra_emails=None, content_subtype='plaintext',
+                                attachment_ids=None, content_subtype='plaintext',
                                 context=None, **kwargs):
         """ Wrapper on message_post, used for user input :
             - mail gateway
@@ -902,35 +957,33 @@ class mail_thread(osv.AbstractModel):
             - 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.
-            - extra_email: [ 'Fabien <fpi@openerp.com>', 'al@openerp.com' ]
         """
-        partner_obj = self.pool.get('res.partner')
         mail_message_obj = self.pool.get('mail.message')
         ir_attachment = self.pool.get('ir.attachment')
-        extra_emails = extra_emails or []
 
-        # 1.A.1: pre-process partners and incoming extra_emails
+        # 1.A.1: add recipients of parent message (# TDE FIXME HACK: mail.thread -> private message)
         partner_ids = set([])
-        for email in extra_emails:
-            partner_id = partner_obj.find_or_create(cr, uid, email, context=context)
-            # link mail with this from mail to the new partner id
-            partner_msg_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
-            if partner_id and partner_msg_ids:
-                mail_message_obj.write(cr, SUPERUSER_ID, partner_msg_ids, {'email_from': None, 'author_id': partner_id}, context=context)
-            partner_ids.add((4, partner_id))
-        if partner_ids:
-            self.message_subscribe(cr, uid, [thread_id], [item[1] for item in partner_ids], context=context)
-
-        # 1.A.2: add recipients of parent message
-        if parent_id:
+        if parent_id and self._name == 'mail.thread':
             parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
             partner_ids |= set([(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:
+            if parent_message.author_id.id:
                 partner_ids.add((4, parent_message.author_id.id))
 
-        # 1.A.3: add specified recipients
-        partner_ids |= set(kwargs.pop('partner_ids', []))
+        # 1.A.2: add specified recipients
+        param_partner_ids = set()
+        for item in kwargs.pop('partner_ids', []):
+            if isinstance(item, (list)):
+                param_partner_ids.add((item[0], item[1]))
+            elif isinstance(item, (int, long)):
+                param_partner_ids.add((4, item))
+            else:
+                param_partner_ids.add(item)
+        partner_ids |= param_partner_ids
+
+        # 1.A.3: add parameters recipients as follower
+        # TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
+        if param_partner_ids and self._name != 'mail.thread':
+            self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
 
         # 1.B: handle body, message_type and message_subtype
         if content_subtype == 'plaintext':
@@ -961,7 +1014,7 @@ class mail_thread(osv.AbstractModel):
         # 3. Post message
         return self.message_post(cr, uid, thread_id=thread_id, body=body,
                             type=msg_type, subtype=msg_subtype, parent_id=parent_id,
-                            attachment_ids=attachment_ids, partner_ids=partner_ids, context=context, **kwargs)
+                            attachment_ids=attachment_ids, partner_ids=list(partner_ids), context=context, **kwargs)
 
     #------------------------------------------------------
     # Followers API
@@ -1015,7 +1068,24 @@ class mail_thread(osv.AbstractModel):
             self.check_access_rights(cr, uid, 'write')
         return self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
 
-    def message_subscribe_from_parent(self, cr, uid, ids, updated_fields, context=None):
+    def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=['user_id'], context=None):
+        """ Returns the list of relational fields linking to res.users that should
+            trigger an auto subscribe. The default list checks for the fields
+            - called 'user_id'
+            - linking to res.users
+            - with track_visibility set
+            In OpenERP V7, this is sufficent for all major addon such as opportunity,
+            project, issue, recruitment, sale.
+            Override this method if a custom behavior is needed about fields
+            that automatically subscribe users.
+        """
+        user_field_lst = []
+        for name, column_info in self._all_columns.items():
+            if name in auto_follow_fields and name in updated_fields and getattr(column_info.column, 'track_visibility', False) and column_info.column._obj == 'res.users':
+                user_field_lst.append(name)
+        return user_field_lst
+
+    def message_auto_subscribe(self, cr, uid, ids, updated_fields, context=None):
         """
             1. fetch project subtype related to task (parent_id.res_model = 'project.task')
             2. for each project subtype: subscribe the follower to the task
@@ -1023,13 +1093,16 @@ class mail_thread(osv.AbstractModel):
         subtype_obj = self.pool.get('mail.message.subtype')
         follower_obj = self.pool.get('mail.followers')
 
+        # fetch auto_follow_fields
+        user_field_lst = self._message_get_auto_subscribe_fields(cr, uid, updated_fields, context=context)
+
         # fetch related record subtypes
         related_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('parent_id.res_model', '=', self._name)], context=context)
         subtypes = subtype_obj.browse(cr, uid, related_subtype_ids, context=context)
         default_subtypes = [subtype for subtype in subtypes if subtype.res_model == False]
         related_subtypes = [subtype for subtype in subtypes if subtype.res_model != False]
         relation_fields = set([subtype.relation_field for subtype in subtypes if subtype.relation_field != False])
-        if not related_subtypes or not any(relation in updated_fields for relation in relation_fields):
+        if (not related_subtypes or not any(relation in updated_fields for relation in relation_fields)) and not user_field_lst:
             return True
 
         for record in self.browse(cr, uid, ids, context=context):
@@ -1051,20 +1124,24 @@ class mail_thread(osv.AbstractModel):
                 for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
                     new_followers.setdefault(follower.partner_id.id, set()).add(subtype.parent_id.id)
 
-            if not parent_res_id or not parent_model:
-                continue
-
-            for subtype in default_subtypes:
-                follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
-                    ('res_model', '=', parent_model),
-                    ('res_id', '=', parent_res_id),
-                    ('subtype_ids', 'in', [subtype.id])
-                    ], context=context)
-                for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
-                    new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id)
+            if parent_res_id and parent_model:
+                for subtype in default_subtypes:
+                    follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
+                        ('res_model', '=', parent_model),
+                        ('res_id', '=', parent_res_id),
+                        ('subtype_ids', 'in', [subtype.id])
+                        ], context=context)
+                    for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
+                        new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id)
+
+            # add followers coming from res.users relational fields that are tracked
+            user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
+            for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]:
+                new_followers.setdefault(partner_id, None)
 
             for pid, subtypes in new_followers.items():
-                self.message_subscribe(cr, uid, [record.id], [pid], list(subtypes), context=context)
+                subtypes = list(subtypes) if subtypes is not None else None
+                self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
         return True
 
     #------------------------------------------------------