[MERGE] Forward-port of latest 7.0 bugfixes, up to rev. 9846 revid:dle@openerp.com...
authorDenis Ledoux <dle@openerp.com>
Mon, 17 Feb 2014 12:42:30 +0000 (13:42 +0100)
committerDenis Ledoux <dle@openerp.com>
Mon, 17 Feb 2014 12:42:30 +0000 (13:42 +0100)
bzr revid: dle@openerp.com-20140214100922-m6rf7c6x85nv67sl
bzr revid: dle@openerp.com-20140214114713-oab4kbearvv7g3nh
bzr revid: dle@openerp.com-20140214131810-9abebxpfeoga1crn
bzr revid: dle@openerp.com-20140217124230-ov201kfep88f5tn7

1  2 
addons/base_vat/base_vat.py
addons/crm/crm_lead.py
addons/event/event.py
addons/fetchmail/fetchmail.py
addons/mail/mail_thread.py
addons/mail/static/src/css/mail.css
addons/mail/tests/test_mail_gateway.py

Simple merge
@@@ -319,18 -343,19 +319,19 @@@ class crm_lead(format_address, osv.osv)
          if partner_id:
              partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
              values = {
 -                'partner_name' : partner.name,
 -                'street' : partner.street,
 -                'street2' : partner.street2,
 -                'city' : partner.city,
 -                'state_id' : partner.state_id and partner.state_id.id or False,
 -                'country_id' : partner.country_id and partner.country_id.id or False,
 -                'email_from' : partner.email,
 -                'phone' : partner.phone,
 -                'mobile' : partner.mobile,
 -                'fax' : partner.fax,
 +                'partner_name': partner.name,
 +                'street': partner.street,
 +                'street2': partner.street2,
 +                'city': partner.city,
 +                'state_id': partner.state_id and partner.state_id.id or False,
 +                'country_id': partner.country_id and partner.country_id.id or False,
 +                'email_from': partner.email,
 +                'phone': partner.phone,
 +                'mobile': partner.mobile,
 +                'fax': partner.fax,
+                 'zip': partner.zip,
              }
 -        return {'value' : values}
 +        return {'value': values}
  
      def on_change_user(self, cr, uid, ids, user_id, context=None):
          """ When changing the user, also set a section_id or restrict section id
Simple merge
Simple merge
@@@ -650,129 -470,27 +650,134 @@@ class mail_thread(osv.AbstractModel)
      def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None):
          """ Find partners related to some header fields of the message.
  
 -            TDE TODO: merge me with other partner finding methods in 8.0 """
 -        partner_obj = self.pool.get('res.partner')
 -        partner_ids = []
 +            :param string message: an email.message instance """
          s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
 -        for email_address in tools.email_split(s):
 -            related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context)
 -            if not related_partners:
 -                related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address)], limit=1, context=context)
 -            partner_ids += related_partners
 -        return partner_ids
 +        return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context))
 +
 +    def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None):
 +        """ Verify route validity. Check and rules:
 +            1 - if thread_id -> check that document effectively exists; otherwise
 +                fallback on a message_new by resetting thread_id
 +            2 - check that message_update exists if thread_id is set; or at least
 +                that message_new exist
 +            [ - find author_id if udpate_author is set]
 +            3 - if there is an alias, check alias_contact:
 +                'followers' and thread_id:
 +                    check on target document that the author is in the followers
 +                'followers' and alias_parent_thread_id:
 +                    check on alias parent document that the author is in the
 +                    followers
 +                'partners': check that author_id id set
 +        """
  
 -    def _message_find_user_id(self, cr, uid, message, context=None):
 -        """ TDE TODO: check and maybe merge me with other user finding methods in 8.0 """
 -        from_local_part = tools.email_split(decode(message.get('From')))[0]
 -        # FP Note: canonification required, the minimu: .lower()
 -        user_ids = self.pool.get('res.users').search(cr, uid, ['|',
 -            ('login', '=', from_local_part),
 -            ('email', '=', from_local_part)], context=context)
 -        return user_ids[0] if user_ids else uid
 +        assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
 +        assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
  
 -    def message_route(self, cr, uid, message, model=None, thread_id=None,
 +        message_id = message.get('Message-Id')
 +        email_from = decode_header(message, 'From')
 +        author_id = message_dict.get('author_id')
 +        model, thread_id, alias = route[0], route[1], route[4]
 +        model_pool = None
 +
 +        def _create_bounce_email():
 +            mail_mail = self.pool.get('mail.mail')
 +            mail_id = mail_mail.create(cr, uid, {
 +                            'body_html': '<div><p>Hello,</p>'
 +                                '<p>The following email sent to %s cannot be accepted because this is '
 +                                'a private email address. Only allowed people can contact us at this address.</p></div>'
 +                                '<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')),
 +                            'subject': 'Re: %s' % message.get('subject'),
 +                            'email_to': message.get('from'),
 +                            'auto_delete': True,
 +                        }, context=context)
 +            mail_mail.send(cr, uid, [mail_id], context=context)
 +
 +        def _warn(message):
 +            _logger.warning('Routing mail with Message-Id %s: route %s: %s',
 +                                message_id, route, message)
 +
 +        # Wrong model
 +        if model and not model in self.pool:
 +            if assert_model:
 +                assert model in self.pool, 'Routing: unknown target model %s' % model
 +            _warn('unknown target model %s' % model)
 +            return ()
 +        elif model:
 +            model_pool = self.pool[model]
 +
 +        # Private message: should not contain any thread_id
 +        if not model and thread_id:
 +            if assert_model:
-                 assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message).'
++                if thread_id: 
++                    raise ValueError('Routing: posting a message without model should be with a null res_id (private message).')
 +            _warn('posting a message without model should be with a null res_id (private message), resetting thread_id')
 +            thread_id = 0
 +        # Private message: should have a parent_id (only answers)
 +        if not model and not message_dict.get('parent_id'):
 +            if assert_model:
-                 assert message_dict.get('parent_id'), 'Routing: posting a message without model should be with a parent_id (private mesage).'
++                if not message_dict.get('parent_id'):
++                    raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).')
 +            _warn('posting a message without model should be with a parent_id (private mesage), skipping')
 +            return ()
 +
 +        # Existing Document: check if exists; if not, fallback on create if allowed
 +        if thread_id and not model_pool.exists(cr, uid, thread_id):
 +            if create_fallback:
 +                _warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id))
 +                thread_id = None
 +            elif assert_model:
 +                assert model_pool.exists(cr, uid, thread_id), 'Routing: reply to missing document (%s,%s)' % (model, thread_id)
 +            else:
 +                _warn('reply to missing document (%s,%s), skipping' % (model, thread_id))
 +                return ()
 +
 +        # Existing Document: check model accepts the mailgateway
 +        if thread_id and model and not hasattr(model_pool, 'message_update'):
 +            if create_fallback:
 +                _warn('model %s does not accept document update, fall back on document creation' % model)
 +                thread_id = None
 +            elif assert_model:
 +                assert hasattr(model_pool, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model
 +            else:
 +                _warn('model %s does not accept document update, skipping' % model)
 +                return ()
 +
 +        # New Document: check model accepts the mailgateway
 +        if not thread_id and model and not hasattr(model_pool, 'message_new'):
 +            if assert_model:
-                 assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model
++                if not hasattr(model_pool, 'message_new'):
++                    raise ValueError(
++                        'Model %s does not accept document creation, crashing' % model
++                    )
 +            _warn('model %s does not accept document creation, skipping' % model)
 +            return ()
 +
 +        # Update message author if asked
 +        # We do it now because we need it for aliases (contact settings)
 +        if not author_id and update_author:
 +            author_ids = self._find_partner_from_emails(cr, uid, thread_id, [email_from], model=model, context=context)
 +            if author_ids:
 +                author_id = author_ids[0]
 +                message_dict['author_id'] = author_id
 +
 +        # Alias: check alias_contact settings
 +        if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id):
 +            if thread_id:
 +                obj = self.pool[model].browse(cr, uid, thread_id, context=context)
 +            else:
 +                obj = self.pool[alias.alias_parent_model_id.model].browse(cr, uid, alias.alias_parent_thread_id, context=context)
 +            if not author_id or not author_id in [fol.id for fol in obj.message_follower_ids]:
 +                _warn('alias %s restricted to internal followers, skipping' % alias.alias_name)
 +                _create_bounce_email()
 +                return ()
 +        elif alias and alias.alias_contact == 'partners' and not author_id:
 +            _warn('alias %s does not accept unknown author, skipping' % alias.alias_name)
 +            _create_bounce_email()
 +            return ()
 +
 +        return (model, thread_id, route[2], route[3], route[4])
 +
 +    def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
                        custom_values=None, context=None):
          """Attempt to figure out the correct target model, thread_id,
          custom_values and user_id to use for an incoming message.
             :param int thread_id: optional ID of the record/thread from ``model``
                 to which this mail should be attached. Only used if the message
                 does not reply to an existing thread and does not match any mail alias.
 -           :return: list of [model, thread_id, custom_values, user_id]
 +           :return: list of [model, thread_id, custom_values, user_id, alias]
+         :raises: ValueError, TypeError
          """
-         assert isinstance(message, Message), 'message must be an email.message.Message at this point'
+         if not isinstance(message, Message):
+             raise TypeError('message must be an email.message.Message at this point')
 +        fallback_model = model
 +
 +        # Get email.message.Message variables for future processing
          message_id = message.get('Message-Id')
          email_from = decode_header(message, 'From')
          email_to = decode_header(message, 'To')
                  thread_id = int(thread_id)
              except:
                  thread_id = False
 -        if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')):
 -            raise ValueError(
 +        _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
 +                    email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
 +        route = self.message_route_verify(cr, uid, message, message_dict,
 +                        (fallback_model, thread_id, custom_values, uid, None),
 +                        update_author=True, assert_model=True, context=context)
 +        if route:
 +            return [route]
 +
 +        # AssertionError if no routes found and if no bounce occured
-         assert False, \
-             "No possible route found for incoming message from %s to %s (Message-Id %s:)." \
-             "Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id)
++        raise ValueError(
+                 'No possible route found for incoming message from %s to %s (Message-Id %s:). '
+                 'Create an appropriate mail.alias or force the destination model.' %
+                 (email_from, email_to, message_id)
+             )
 -        if thread_id and not model_pool.exists(cr, uid, thread_id):
 -            _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
 -                                thread_id, message_id)
 -            thread_id = None
 -        _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
 -                        email_from, email_to, message_id, model, thread_id, custom_values, uid)
 -        return [(model, thread_id, custom_values, uid)]
 +
 +    def message_route_process(self, cr, uid, message, message_dict, routes, context=None):
 +        # postpone setting message_dict.partner_ids after message_post, to avoid double notifications
 +        partner_ids = message_dict.pop('partner_ids', [])
 +        thread_id = False
 +        for model, thread_id, custom_values, user_id, alias in routes:
 +            if self._name == 'mail.thread':
 +                context.update({'thread_model': model})
 +            if model:
 +                model_pool = self.pool[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" % \
-                     (message_dict['message_id'], model)
++                if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')):
++                    raise ValueError(
++                        "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" %
++                        (message_dict['message_id'], model)
++                    )
 +
 +                # disabled subscriptions during message_new/update to avoid having the system user running the
 +                # email gateway become a follower of all inbound messages
 +                nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
 +                if thread_id and hasattr(model_pool, 'message_update'):
 +                    model_pool.message_update(cr, user_id, [thread_id], message_dict, context=nosub_ctx)
 +                else:
 +                    thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
 +            else:
-                 assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
++                if thread_id:
++                    raise ValueError("Posting a message without model should be with a null res_id, to create a private message.")
 +                model_pool = self.pool.get('mail.thread')
 +            if not hasattr(model_pool, 'message_post'):
 +                context['thread_model'] = model
 +                model_pool = self.pool['mail.thread']
 +            new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **message_dict)
 +
 +            if partner_ids:
 +                # postponed after message_post, because this is an external message and we don't want to create
 +                # duplicate emails due to notifications
 +                self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
 +        return thread_id
  
      def message_process(self, cr, uid, model, message, custom_values=None,
                          save_original=False, strip_attachments=False,
Simple merge
@@@ -465,18 -411,18 +465,18 @@@ class TestMailgateway(TestMail)
          # --------------------------------------------------
  
          # Do: incoming email with model that does not accepts incoming emails must raise
-         self.assertRaises(AssertionError,
+         self.assertRaises(ValueError,
 -                            format_and_process,
 -                            MAIL_TEMPLATE,
 -                            to='noone@example.com', subject='spam', extra='', model='res.country',
 -                            msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>')
 +                          format_and_process,
 +                          MAIL_TEMPLATE,
 +                          to='noone@example.com', subject='spam', extra='', model='res.country',
 +                          msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>')
  
          # Do: incoming email without model and without alias must raise
-         self.assertRaises(AssertionError,
+         self.assertRaises(ValueError,
 -                            format_and_process,
 -                            MAIL_TEMPLATE,
 -                            to='noone@example.com', subject='spam', extra='',
 -                            msg_id='<1198923581.41972151344608186760.JavaMail.new5@agrolait.com>')
 +                          format_and_process,
 +                          MAIL_TEMPLATE,
 +                          to='noone@example.com', subject='spam', extra='',
 +                          msg_id='<1198923581.41972151344608186760.JavaMail.new5@agrolait.com>')
  
          # Do: incoming email with model that accepting incoming emails as fallback
          frog_groups = format_and_process(MAIL_TEMPLATE,