[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

@@@ -54,7 -54,7 +54,7 @@@ _ref_vat = 
      'gr': 'GR12345670',
      'hu': 'HU12345676',
      'hr': 'HR01234567896', # Croatia, contributed by Milan Tribuson 
-     'ie': 'IE1234567T',
+     'ie': 'IE1234567FA',
      'it': 'IT12345670017',
      'lt': 'LT123456715',
      'lu': 'LU12345613',
@@@ -190,6 -190,34 +190,34 @@@ class res_partner(osv.osv)
              return check == int(num[8])
          return False
  
+     def _ie_check_char(self, vat):
+         vat = vat.zfill(8)
+         extra = 0
+         if vat[7] not in ' W':
+             if vat[7].isalpha():
+                 extra = 9 * (ord(vat[7]) - 64)
+             else:
+                 # invalid
+                 return -1
+         checksum = extra + sum((8-i) * int(x) for i, x in enumerate(vat[:7]))
+         return 'WABCDEFGHIJKLMNOPQRSTUV'[checksum % 23]
+     def check_vat_ie(self, vat):
+         """ Temporary Ireland VAT validation to support the new format
+         introduced in January 2013 in Ireland, until upstream is fixed.
+         TODO: remove when fixed upstream"""
+         if len(vat) not in (8, 9) or not vat[2:7].isdigit():
+             return False
+         if len(vat) == 8:
+             # Normalize pre-2013 numbers: final space or 'W' not significant
+             vat += ' '
+         if vat[:7].isdigit():
+             return vat[7] == self._ie_check_char(vat[:7] + vat[8])
+         elif vat[1] in (string.ascii_uppercase + '+*'):
+             # Deprecated format
+             # See http://www.revenue.ie/en/online/third-party-reporting/reporting-payment-details/faqs.html#section3
+             return vat[7] == self._ie_check_char(vat[2:7] + vat[0] + vat[8])
+         return False
  
      # Mexican VAT verification, contributed by <moylop260@hotmail.com>
      # and Panos Christeas <p_christ@hol.gr>
              return False
          return check == int(vat[8])
  
 -res_partner()
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --combined addons/crm/crm_lead.py
  #
  ##############################################################################
  
  import crm
  from datetime import datetime
  from operator import itemgetter
 -from openerp.osv import fields, osv, orm
 -import time
 +
  from openerp import SUPERUSER_ID
  from openerp import tools
 -from openerp.tools.translate import _
 +from openerp.addons.base.res.res_partner import format_address
 +from openerp.osv import fields, osv, orm
  from openerp.tools import html2plaintext
 -
 -from base.res.res_partner import format_address
 +from openerp.tools.translate import _
  
  CRM_LEAD_FIELDS_TO_MERGE = ['name',
      'partner_id',
      'email_from',
      'email_cc',
      'partner_name']
 -CRM_LEAD_PENDING_STATES = (
 -    crm.AVAILABLE_STATES[2][0], # Cancelled
 -    crm.AVAILABLE_STATES[3][0], # Done
 -    crm.AVAILABLE_STATES[4][0], # Pending
 -)
  
 -class crm_lead(base_stage, format_address, osv.osv):
 +
 +class crm_lead(format_address, osv.osv):
      """ CRM Lead Case """
      _name = "crm.lead"
      _description = "Lead/Opportunity"
      _inherit = ['mail.thread', 'ir.needaction_mixin']
  
      _track = {
 -        'state': {
 -            'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'],
 -            'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
 -            'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancel',
 -        },
          'stage_id': {
 -            'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'cancel', 'done'],
 +            # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
 +            'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.sequence <= 1,
 +            'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: (obj.stage_id and obj.stage_id.sequence > 1) and obj.probability < 100,
 +            'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.probability == 100 and obj.stage_id and obj.stage_id.fold,
 +            'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1,
          },
      }
  
 -    def create(self, cr, uid, vals, context=None):
 -        if context is None:
 -            context = {}
 -        if vals.get('type') and not context.get('default_type'):
 -            context['default_type'] = vals.get('type')
 -        if vals.get('section_id') and not context.get('default_section_id'):
 -            context['default_section_id'] = vals.get('section_id')
 -
 -        # context: no_log, because subtype already handle this
 -        create_context = dict(context, mail_create_nolog=True)
 -        return super(crm_lead, self).create(cr, uid, vals, context=create_context)
 +    def get_empty_list_help(self, cr, uid, help, context=None):
 +        if context.get('default_type') == 'lead':
 +            context['empty_list_help_model'] = 'crm.case.section'
 +            context['empty_list_help_id'] = context.get('default_section_id')
 +        context['empty_list_help_document_name'] = _("leads")
 +        return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context)
  
      def _get_default_section_id(self, cr, uid, context=None):
          """ Gives default section by checking if present in the context """
      def _get_default_stage_id(self, cr, uid, context=None):
          """ Gives default stage_id """
          section_id = self._get_default_section_id(cr, uid, context=context)
 -        return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft')], context=context)
 +        return self.stage_find(cr, uid, [], section_id, [('fold', '=', False)], context=context)
  
      def _resolve_section_id_from_context(self, cr, uid, context=None):
          """ Returns ID of section based on the value of 'section_id'
          if type(context.get('default_section_id')) in (int, long):
              return context.get('default_section_id')
          if isinstance(context.get('default_section_id'), basestring):
 -            section_name = context['default_section_id']
 -            section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=section_name, context=context)
 +            section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
              if len(section_ids) == 1:
                  return int(section_ids[0][0])
          return None
          stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
          result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
          # restore order of the search
 -        result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
 +        result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
  
          fold = {}
          for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
          return result, fold
  
      def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
 -        res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
 +        res = super(crm_lead, self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
          if view_type == 'form':
              res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
          return res
                  res[lead.id][field] = abs(int(duration))
          return res
  
 -    def _history_search(self, cr, uid, obj, name, args, context=None):
 -        res = []
 -        msg_obj = self.pool.get('mail.message')
 -        message_ids = msg_obj.search(cr, uid, [('email_from','!=',False), ('subject', args[0][1], args[0][2])], context=context)
 -        lead_ids = self.search(cr, uid, [('message_ids', 'in', message_ids)], context=context)
 -
 -        if lead_ids:
 -            return [('id', 'in', lead_ids)]
 -        else:
 -            return [('id', '=', '0')]
 -
      _columns = {
          'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', track_visibility='onchange',
              select=True, help="Linked partner (optional). Usually created when converting the lead."),
          'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
          'section_id': fields.many2one('crm.case.section', 'Sales Team',
                          select=True, track_visibility='onchange', help='When sending mails, the default email address is taken from the sales team.'),
 -        'create_date': fields.datetime('Creation Date' , readonly=True),
 -        'email_cc': fields.text('Global CC', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
 +        'create_date': fields.datetime('Creation Date', readonly=True),
 +        'email_cc': fields.text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
          'description': fields.text('Notes'),
 -        'write_date': fields.datetime('Update Date' , readonly=True),
 +        'write_date': fields.datetime('Update Date', readonly=True),
          'categ_ids': fields.many2many('crm.case.categ', 'crm_lead_category_rel', 'lead_id', 'category_id', 'Categories', \
              domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"),
          'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \
          'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
          'date_closed': fields.datetime('Closed', readonly=True),
          'stage_id': fields.many2one('crm.case.stage', 'Stage', track_visibility='onchange',
 -                        domain="['&', '&', ('fold', '=', False), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
 +                        domain="['&', ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"),
          'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'),
          'referred': fields.char('Referred By', size=64),
 -        'date_open': fields.datetime('Opened', readonly=True),
 +        'date_open': fields.datetime('Assigned', readonly=True),
          'day_open': fields.function(_compute_day, string='Days to Open', \
                                  multi='day_open', type="float", store=True),
          'day_close': fields.function(_compute_day, string='Days to Close', \
                                  multi='day_close', type="float", store=True),
 -        'state': fields.related('stage_id', 'state', type="selection", store=True,
 -                selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
 -                help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is  set to \'Pending\'.'),
 +        'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
  
 +        # Messaging and marketing
 +        'message_bounce': fields.integer('Bounce'),
          # Only used for type opportunity
 -        'probability': fields.float('Success Rate (%)',group_operator="avg"),
 +        'probability': fields.float('Success Rate (%)', group_operator="avg"),
          'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
          'ref': fields.reference('Reference', selection=crm._links_get, size=128),
          'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
      _defaults = {
          'active': 1,
          'type': 'lead',
 -        'user_id': lambda s, cr, uid, c: s._get_default_user(cr, uid, c),
 -        'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
 +        'user_id': lambda s, cr, uid, c: uid,
          'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
          'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
          'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c),
          'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
          'color': 0,
 +        'date_last_stage_update': fields.datetime.now,
      }
  
      _sql_constraints = [
  
      def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
          if not stage_id:
 -            return {'value':{}}
 -        stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context)
 +            return {'value': {}}
 +        stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
          if not stage.on_change:
 -            return {'value':{}}
 -        return {'value':{'probability': stage.probability}}
 +            return {'value': {}}
 +        return {'value': {'probability': stage.probability}}
  
 -    def on_change_partner(self, cr, uid, ids, partner_id, context=None):
 +    def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
          values = {}
          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
              to the ones user_id is member of. """
 -        if user_id:
 +        section_id = self._get_default_section_id(cr, uid, context=context) or False
 +        if user_id and not section_id:
              section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
              if section_ids:
 -                return {'value': {'section_id': section_ids[0]}}
 -        return {'value': {}}
 -
 -    def _check(self, cr, uid, ids=False, context=None):
 -        """ Override of the base.stage method.
 -            Function called by the scheduler to process cases for date actions
 -            Only works on not done and cancelled cases
 -        """
 -        cr.execute('select * from crm_case \
 -                where (date_action_last<%s or date_action_last is null) \
 -                and (date_action_next<=%s or date_action_next is null) \
 -                and state not in (\'cancel\',\'done\')',
 -                (time.strftime("%Y-%m-%d %H:%M:%S"),
 -                    time.strftime('%Y-%m-%d %H:%M:%S')))
 -
 -        ids2 = map(lambda x: x[0], cr.fetchall() or [])
 -        cases = self.browse(cr, uid, ids2, context=context)
 -        return self._action(cr, uid, cases, False, context=context)
 +                section_id = section_ids[0]
 +        return {'value': {'section_id': section_id}}
  
      def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None):
          """ Override of the base.stage method
          """
          if isinstance(cases, (int, long)):
              cases = self.browse(cr, uid, cases, context=context)
 +        if context is None:
 +            context = {}
 +        # check whether we should try to add a condition on type
 +        avoid_add_type_term = any([term for term in domain if len(term) == 3 if term[0] == 'type'])
          # collect all section_ids
 -        section_ids = []
 +        section_ids = set()
          types = ['both']
 -        if not cases :
 -            type = context.get('default_type')
 -            types += [type]
 +        if not cases and context.get('default_type'):
 +            ctx_type = context.get('default_type')
 +            types += [ctx_type]
          if section_id:
 -            section_ids.append(section_id)
 +            section_ids.add(section_id)
          for lead in cases:
              if lead.section_id:
 -                section_ids.append(lead.section_id.id)
 +                section_ids.add(lead.section_id.id)
              if lead.type not in types:
                  types.append(lead.type)
          # OR all section_ids and OR with case_default
              search_domain += [('|')] * len(section_ids)
              for section_id in section_ids:
                  search_domain.append(('section_ids', '=', section_id))
 -        else:
 -            search_domain.append(('case_default', '=', True))
 +        search_domain.append(('case_default', '=', True))
          # AND with cases types
 -        search_domain.append(('type', 'in', types))
 +        if not avoid_add_type_term:
 +            search_domain.append(('type', 'in', types))
          # AND with the domain in parameter
          search_domain += list(domain)
          # perform search, return the first found
 -        stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, context=context)
 +        stage_ids = self.pool.get('crm.case.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
          if stage_ids:
              return stage_ids[0]
          return False
  
 -    def case_cancel(self, cr, uid, ids, context=None):
 -        """ Overrides case_cancel from base_stage to set probability """
 -        res = super(crm_lead, self).case_cancel(cr, uid, ids, context=context)
 -        self.write(cr, uid, ids, {'probability' : 0.0}, context=context)
 -        return res
 -
 -    def case_reset(self, cr, uid, ids, context=None):
 -        """ Overrides case_reset from base_stage to set probability """
 -        res = super(crm_lead, self).case_reset(cr, uid, ids, context=context)
 -        self.write(cr, uid, ids, {'probability': 0.0}, context=context)
 -        return res
 -
      def case_mark_lost(self, cr, uid, ids, context=None):
 -        """ Mark the case as lost: state=cancel and probability=0 """
 -        for lead in self.browse(cr, uid, ids):
 -            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0),('on_change','=',True)], context=context)
 +        """ Mark the case as lost: state=cancel and probability=0
 +            :deprecated: this method will be removed in OpenERP v8.
 +        """
 +        stages_leads = {}
 +        for lead in self.browse(cr, uid, ids, context=context):
 +            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0), ('fold', '=', True), ('sequence', '>', 1)], context=context)
              if stage_id:
 -                self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
 +                if stages_leads.get(stage_id):
 +                    stages_leads[stage_id].append(lead.id)
 +                else:
 +                    stages_leads[stage_id] = [lead.id]
 +            else:
 +                raise osv.except_osv(_('Warning!'),
 +                    _('To relieve your sales pipe and group all Lost opportunities, configure one of your sales stage as follow:\n'
 +                        'probability = 0 %, select "Change Probability Automatically".\n'
 +                        'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
 +        for stage_id, lead_ids in stages_leads.items():
 +            self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
          return True
  
      def case_mark_won(self, cr, uid, ids, context=None):
 -        """ Mark the case as won: state=done and probability=100 """
 -        for lead in self.browse(cr, uid, ids):
 -            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0),('on_change','=',True)], context=context)
 +        """ Mark the case as won: state=done and probability=100
 +            :deprecated: this method will be removed in OpenERP v8.
 +        """
 +        stages_leads = {}
 +        for lead in self.browse(cr, uid, ids, context=context):
 +            stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0), ('fold', '=', True)], context=context)
              if stage_id:
 -                self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
 +                if stages_leads.get(stage_id):
 +                    stages_leads[stage_id].append(lead.id)
 +                else:
 +                    stages_leads[stage_id] = [lead.id]
 +            else:
 +                raise osv.except_osv(_('Warning!'),
 +                    _('To relieve your sales pipe and group all Won opportunities, configure one of your sales stage as follow:\n'
 +                        'probability = 100 % and select "Change Probability Automatically".\n'
 +                        'Create a specific stage or edit an existing one by editing columns of your opportunity pipe.'))
 +        for stage_id, lead_ids in stages_leads.items():
 +            self.write(cr, uid, lead_ids, {'stage_id': stage_id}, context=context)
 +        return True
 +
 +    def case_escalate(self, cr, uid, ids, context=None):
 +        """ Escalates case to parent level """
 +        for case in self.browse(cr, uid, ids, context=context):
 +            data = {'active': True}
 +            if case.section_id.parent_id:
 +                data['section_id'] = case.section_id.parent_id.id
 +                if case.section_id.parent_id.change_responsible:
 +                    if case.section_id.parent_id.user_id:
 +                        data['user_id'] = case.section_id.parent_id.user_id.id
 +            else:
 +                raise osv.except_osv(_('Error!'), _("You are already at the top level of your sales-team category.\nTherefore you cannot escalate furthermore."))
 +            self.write(cr, uid, [case.id], data, context=context)
          return True
  
 -    def set_priority(self, cr, uid, ids, priority):
 +    def set_priority(self, cr, uid, ids, priority, context=None):
          """ Set lead priority
          """
 -        return self.write(cr, uid, ids, {'priority' : priority})
 +        return self.write(cr, uid, ids, {'priority': priority}, context=context)
  
      def set_high_priority(self, cr, uid, ids, context=None):
          """ Set lead priority to high
          """
 -        return self.set_priority(cr, uid, ids, '1')
 +        return self.set_priority(cr, uid, ids, '1', context=context)
  
      def set_normal_priority(self, cr, uid, ids, context=None):
          """ Set lead priority to normal
          """
 -        return self.set_priority(cr, uid, ids, '3')
 +        return self.set_priority(cr, uid, ids, '3', context=context)
  
      def _merge_get_result_type(self, cr, uid, opps, context=None):
          """
                  attachment.write(values)
          return True
  
 -    def merge_opportunity(self, cr, uid, ids, context=None):
 +    def merge_opportunity(self, cr, uid, ids, user_id=False, section_id=False, context=None):
          """
          Different cases of merge:
          - merge leads together = 1 new lead
  
          opportunities = self.browse(cr, uid, ids, context=context)
          sequenced_opps = []
 +        # Sorting the leads/opps according to the confidence level of its stage, which relates to the probability of winning it
 +        # The confidence level increases with the stage sequence, except when the stage probability is 0.0 (Lost cases)
 +        # An Opportunity always has higher confidence level than a lead, unless its stage probability is 0.0
          for opportunity in opportunities:
              sequence = -1
 -            if opportunity.stage_id and opportunity.stage_id.state != 'cancel':
 +            if opportunity.stage_id and not opportunity.stage_id.fold:
                  sequence = opportunity.stage_id.sequence
              sequenced_opps.append(((int(sequence != -1 and opportunity.type == 'opportunity'), sequence, -opportunity.id), opportunity))
  
          fields = list(CRM_LEAD_FIELDS_TO_MERGE)
          merged_data = self._merge_data(cr, uid, ids, highest, fields, context=context)
  
 +        if user_id:
 +            merged_data['user_id'] = user_id
 +        if section_id:
 +            merged_data['section_id'] = section_id
 +
          # Merge messages and attachements into the first opportunity
          self._merge_opportunity_history(cr, uid, highest.id, tail_opportunities, context=context)
          self._merge_opportunity_attachments(cr, uid, highest.id, tail_opportunities, context=context)
              'phone': customer and customer.phone or lead.phone,
          }
          if not lead.stage_id or lead.stage_id.type=='lead':
 -            val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('state', '=', 'draft'),('type', 'in', ('opportunity','both'))], context=context)
 +            val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('type', 'in', ('opportunity', 'both'))], context=context)
          return val
  
      def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
              partner = self.pool.get('res.partner')
              customer = partner.browse(cr, uid, partner_id, context=context)
          for lead in self.browse(cr, uid, ids, context=context):
 -            if lead.state in ('done', 'cancel'):
 +            # TDE: was if lead.state in ('done', 'cancel'):
 +            if lead.probability == 100 or (lead.probability == 0 and lead.stage_id.fold):
                  continue
              vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
              self.write(cr, uid, [lead.id], vals, context=context)
 -        self.message_post(cr, uid, ids, body=_("Lead <b>converted into an Opportunity</b>"), subtype="crm.mt_lead_convert_to_opportunity", context=context)
  
          if user_ids or section_id:
              self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
                  'priority': lead.priority,
              }
              new_id = phonecall.create(cr, uid, vals, context=context)
 -            phonecall.case_open(cr, uid, [new_id], context=context)
 +            phonecall.write(cr, uid, [new_id], {'state': 'open'}, context=context)
              if action == 'log':
 -                phonecall.case_close(cr, uid, [new_id], context=context)
 +                phonecall.write(cr, uid, [new_id], {'state': 'done'}, context=context)
              phonecall_dict[lead.id] = new_id
              self.schedule_phonecall_send_note(cr, uid, [lead.id], new_id, action, context=context)
          return phonecall_dict
          }
          return res
  
 +    def create(self, cr, uid, vals, context=None):
 +        if context is None:
 +            context = {}
 +        if vals.get('type') and not context.get('default_type'):
 +            context['default_type'] = vals.get('type')
 +        if vals.get('section_id') and not context.get('default_section_id'):
 +            context['default_section_id'] = vals.get('section_id')
 +
 +        # context: no_log, because subtype already handle this
 +        create_context = dict(context, mail_create_nolog=True)
 +        return super(crm_lead, self).create(cr, uid, vals, context=create_context)
 +
      def write(self, cr, uid, ids, vals, context=None):
 +        # stage change: update date_last_stage_update
 +        if 'stage_id' in vals:
 +            vals['date_last_stage_update'] = fields.datetime.now()
 +        # stage change with new stage: update probability
          if vals.get('stage_id') and not vals.get('probability'):
 -            # change probability of lead(s) if required by stage
 -            stage = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context)
 -            if stage.on_change:
 -                vals['probability'] = stage.probability
 +            onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']
 +            vals.update(onchange_stage_values)
          return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
  
      def copy(self, cr, uid, id, default=None, context=None):
          default['stage_id'] = self._get_default_stage_id(cr, uid, local_context)
          return super(crm_lead, self).copy(cr, uid, id, default, context=context)
  
 -    def new_mail_send(self, cr, uid, ids, context=None):
 -        '''
 -        This function opens a window to compose an email, with the edi sale template message loaded by default
 -        '''
 -        assert len(ids) == 1, 'This option should only be used for a single id at a time.'
 -        ir_model_data = self.pool.get('ir.model.data')
 -        try:
 -            template_id = ir_model_data.get_object_reference(cr, uid, 'crm', 'email_template_opportunity_mail')[1]
 -        except ValueError:
 -            template_id = False
 -        try:
 -            compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
 -        except ValueError:
 -            compose_form_id = False
 -        if context is None:
 -            context = {}
 -        ctx = context.copy()
 -        ctx.update({
 -            'default_model': 'crm.lead',
 -            'default_res_id': ids[0],
 -            'default_use_template': bool(template_id),
 -            'default_template_id': template_id,
 -            'default_composition_mode': 'comment',
 -        })
 -        return {
 -            'type': 'ir.actions.act_window',
 -            'view_type': 'form',
 -            'view_mode': 'form',
 -            'res_model': 'mail.compose.message',
 -            'views': [(compose_form_id, 'form')],
 -            'view_id': compose_form_id,
 -            'target': 'new',
 -            'context': ctx,
 -        }
 -
      # ----------------------------------------
      # Mail Gateway
      # ----------------------------------------
          return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
                      for lead in self.browse(cr, SUPERUSER_ID, ids, context=context)]
  
 +    def _get_formview_action(self, cr, uid, id, context=None):
 +        action = super(crm_lead, self)._get_formview_action(cr, uid, id, context=context)
 +        obj = self.browse(cr, uid, id, context=context)
 +        if obj.type == 'opportunity':
 +            model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
 +            action.update({
 +                'views': [(view_id, 'form')],
 +                })
 +        return action
 +
      def message_get_suggested_recipients(self, cr, uid, ids, context=None):
          recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
          try:
          """
          if custom_values is None:
              custom_values = {}
 -        desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
          defaults = {
              'name':  msg.get('subject') or _("No Subject"),
 -            'description': desc,
              'email_from': msg.get('from'),
              'email_cc': msg.get('cc'),
              'partner_id': msg.get('author_id', False),
              'user_id': False,
          }
          if msg.get('author_id'):
 -            defaults.update(self.on_change_partner(cr, uid, None, msg.get('author_id'), context=context)['value'])
 +            defaults.update(self.on_change_partner_id(cr, uid, None, msg.get('author_id'), context=context)['value'])
          if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
              defaults['priority'] = msg.get('priority')
          defaults.update(custom_values)
diff --combined addons/event/event.py
@@@ -41,6 -41,7 +41,6 @@@ class event_type(osv.osv)
          'default_registration_max': 0,
      }
  
 -event_type()
  
  class event_event(osv.osv):
      """Event"""
                          continue
          return res
  
 +    def _get_visibility_selection(self, cr, uid, context=None):
 +        return [('public', 'All Users'),
 +                ('employees', 'Employees Only')]
 +    # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
 +    _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs)
 +
      _columns = {
          'name': fields.char('Name', size=64, required=True, translate=True, readonly=False, states={'done': [('readonly', True)]}),
          'user_id': fields.many2one('res.users', 'Responsible User', readonly=False, states={'done': [('readonly', True)]}),
          'note': fields.text('Description', readonly=False, states={'done': [('readonly', True)]}),
          'company_id': fields.many2one('res.company', 'Company', required=False, change_default=True, readonly=False, states={'done': [('readonly', True)]}),
          'is_subscribed' : fields.function(_subscribe_fnc, type="boolean", string='Subscribed'),
 +        'visibility': fields.selection(_visibility_selection, 'Privacy / Visibility',
 +            select=True, required=True),
      }
      _defaults = {
          'state': 'draft',
          'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'event.event', context=c),
          'user_id': lambda obj, cr, uid, context: uid,
 +        'visibility': 'employees',
      }
  
      def subscribe_to_event(self, cr, uid, ids, context=None):
      ]
  
      def onchange_event_type(self, cr, uid, ids, type_event, context=None):
+         values = {}
          if type_event:
              type_info =  self.pool.get('event.type').browse(cr,uid,type_event,context)
              dic ={
                'register_min': type_info.default_registration_min,
                'register_max': type_info.default_registration_max,
              }
-             return {'value': dic}
+             values.update(dic)
+         return values
  
      def on_change_address_id(self, cr, uid, ids, address_id, context=None):
          values = {}
@@@ -34,6 -34,7 +34,6 @@@ import zipfil
  import base64
  from openerp import addons
  
 -from openerp import netsvc
  from openerp.osv import fields, osv
  from openerp import tools
  from openerp.tools.translate import _
@@@ -187,7 -188,7 +187,7 @@@ openerp_mailgate: "|/path/to/openerp-ma
          for server in self.browse(cr, uid, ids, context=context):
              _logger.info('start checking for new emails on %s server %s', server.type, server.name)
              context.update({'fetchmail_server_id': server.id, 'server_type': server.type})
-             count = 0
+             count, failed = 0, 0
              imap_server = False
              pop_server = False
              if server.type == 'imap':
                      imap_server.select()
                      result, data = imap_server.search(None, '(UNSEEN)')
                      for num in data[0].split():
+                         res_id = None
                          result, data = imap_server.fetch(num, '(RFC822)')
-                         res_id = mail_thread.message_process(cr, uid, server.object_id.model, 
-                                                              data[0][1],
-                                                              save_original=server.original,
-                                                              strip_attachments=(not server.attach),
-                                                              context=context)
+                         imap_server.store(num, '-FLAGS', '\\Seen')
+                         try:
+                             res_id = mail_thread.message_process(cr, uid, server.object_id.model,
+                                                                  data[0][1],
+                                                                  save_original=server.original,
+                                                                  strip_attachments=(not server.attach),
+                                                                  context=context)
+                         except Exception:
+                             _logger.exception('Failed to process mail from %s server %s.', server.type, server.name)
+                             failed += 1
                          if res_id and server.action_id:
-                             action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id], 'active_model': context.get("thread_model", server.object_id.model)})
-                             imap_server.store(num, '+FLAGS', '\\Seen')
-                             cr.commit()
+                             action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids': [res_id], 'active_model': context.get("thread_model", server.object_id.model)})
+                         imap_server.store(num, '+FLAGS', '\\Seen')
+                         cr.commit()
                          count += 1
-                     _logger.info("fetched/processed %s email(s) on %s server %s", count, server.type, server.name)
+                     _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed)
                  except Exception:
-                     _logger.exception("Failed to fetch mail from %s server %s.", server.type, server.name)
+                     _logger.exception("General failure when trying to fetch mail from %s server %s.", server.type, server.name)
                  finally:
                      if imap_server:
                          imap_server.close()
                      for num in range(1, numMsgs + 1):
                          (header, msges, octets) = pop_server.retr(num)
                          msg = '\n'.join(msges)
-                         res_id = mail_thread.message_process(cr, uid, server.object_id.model,
-                                                              msg,
-                                                              save_original=server.original,
-                                                              strip_attachments=(not server.attach),
-                                                              context=context)
+                         res_id = None
+                         try:
+                             res_id = mail_thread.message_process(cr, uid, server.object_id.model,
+                                                                  msg,
+                                                                  save_original=server.original,
+                                                                  strip_attachments=(not server.attach),
+                                                                  context=context)
+                         except Exception:
+                             _logger.exception('Failed to process mail from %s server %s.', server.type, server.name)
+                             failed += 1
                          if res_id and server.action_id:
-                             action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id], 'active_model': context.get("thread_model", server.object_id.model)})
+                             action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids': [res_id], 'active_model': context.get("thread_model", server.object_id.model)})
                          pop_server.dele(num)
                          cr.commit()
-                     _logger.info("fetched/processed %s email(s) on %s server %s", numMsgs, server.type, server.name)
+                     _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", numMsgs, server.type, server.name, (numMsgs - failed), failed)
                  except Exception:
-                     _logger.exception("Failed to fetch mail from %s server %s.", server.type, server.name)
+                     _logger.exception("General failure when trying to fetch mail from %s server %s.", server.type, server.name)
                  finally:
                      if pop_server:
                          pop_server.quit()
@@@ -25,6 -25,7 +25,6 @@@ import dateuti
  import email
  import logging
  import pytz
 -import re
  import time
  import xmlrpclib
  from email.message import Message
@@@ -71,7 -72,6 +71,7 @@@ class mail_thread(osv.AbstractModel)
      _name = 'mail.thread'
      _description = 'Email Thread'
      _mail_flat_thread = True
 +    _mail_post_access = 'write'
  
      # Automatic logging system if mail installed
      # _track = {
      #   :param function lambda: returns whether the tracking should record using this subtype
      _track = {}
  
 +    def get_empty_list_help(self, cr, uid, help, context=None):
 +        """ Override of BaseModel.get_empty_list_help() to generate an help message
 +            that adds alias information. """
 +        model = context.get('empty_list_help_model')
 +        res_id = context.get('empty_list_help_id')
 +        ir_config_parameter = self.pool.get("ir.config_parameter")
 +        catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
 +        document_name = context.get('empty_list_help_document_name', _('document'))
 +        alias = None
 +
 +        if catchall_domain and model and res_id:  # specific res_id -> find its alias (i.e. section_id specified)
 +            object_id = self.pool.get(model).browse(cr, uid, res_id, context=context)
 +            # check that the alias effectively creates new records
 +            if object_id.alias_id and object_id.alias_id.alias_name and \
 +                    object_id.alias_id.alias_model_id and \
 +                    object_id.alias_id.alias_model_id.model == self._name and \
 +                    object_id.alias_id.alias_force_thread_id == 0:
 +                alias = object_id.alias_id
 +        elif catchall_domain and model:  # no specific res_id given -> generic help message, take an example alias (i.e. alias of some section_id)
 +            model_id = self.pool.get('ir.model').search(cr, uid, [("model", "=", self._name)], context=context)[0]
 +            alias_obj = self.pool.get('mail.alias')
 +            alias_ids = alias_obj.search(cr, uid, [("alias_model_id", "=", model_id), ("alias_name", "!=", False), ('alias_force_thread_id', '=', 0)], context=context, order='id ASC')
 +            if alias_ids and len(alias_ids) == 1:  # if several aliases -> incoherent to propose one guessed from nowhere, therefore avoid if several aliases
 +                alias = alias_obj.browse(cr, uid, alias_ids[0], context=context)
 +
 +        if alias:
 +            alias_email = alias.name_get()[0][1]
 +            return _("""<p class='oe_view_nocontent_create'>
 +                            Click here to add new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a>
 +                        </p>
 +                        %(static_help)s"""
 +                    ) % {
 +                        'document': document_name,
 +                        'email': alias_email,
 +                        'static_help': help or ''
 +                    }
 +
 +        if document_name != 'document' and help and help.find("oe_view_nocontent_create") == -1:
 +            return _("<p class='oe_view_nocontent_create'>Click here to add new %(document)s</p>%(static_help)s") % {
 +                        'document': document_name,
 +                        'static_help': help or '',
 +                    }
 +
 +        return help
 +
      def _get_message_data(self, cr, uid, ids, name, args, context=None):
          """ Computes:
              - message_unread: has uid unread message for the document
                  res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New"))
          return res
  
 -    def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
 +    def read_followers_data(self, cr, uid, follower_ids, context=None):
 +        result = []
 +        technical_group = self.pool.get('ir.model.data').get_object(cr, uid, 'base', 'group_no_one')
 +        for follower in self.pool.get('res.partner').browse(cr, uid, follower_ids, context=context):
 +            is_editable = uid in map(lambda x: x.id, technical_group.users)
 +            is_uid = uid in map(lambda x: x.id, follower.user_ids)
 +            data = (follower.id,
 +                    follower.name,
 +                    {'is_editable': is_editable, 'is_uid': is_uid},
 +                    )
 +            result.append(data)
 +        return result
 +
 +    def _get_subscription_data(self, cr, uid, ids, name, args, user_pid=None, context=None):
          """ Computes:
              - message_subtype_data: data about document subtypes: which are
                  available, which are followed if any """
          res = dict((id, dict(message_subtype_data='')) for id in ids)
 -        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
 +        if user_pid is None:
 +            user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
  
          # find current model subtypes, add them to a dictionary
          subtype_obj = self.pool.get('mail.message.subtype')
          self.message_subscribe(cr, uid, [id], list(new-old), context=context)
  
      def _search_followers(self, cr, uid, obj, name, args, context):
 +        """Search function for message_follower_ids
 +
 +        Do not use with operator 'not in'. Use instead message_is_followers
 +        """
          fol_obj = self.pool.get('mail.followers')
          res = []
          for field, operator, value in args:
              assert field == name
 +            # TOFIX make it work with not in
 +            assert operator != "not in", "Do not search message_follower_ids with 'not in'"
              fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)])
              res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)]
              res.append(('id', 'in', res_ids))
          return res
  
 +    def _search_is_follower(self, cr, uid, obj, name, args, context):
 +        """Search function for message_is_follower"""
 +        res = []
 +        for field, operator, value in args:
 +            assert field == name
 +            partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
 +            if (operator == '=' and value) or (operator == '!=' and not value):  # is a follower
 +                res_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
 +            else:  # is not a follower or unknown domain
 +                mail_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
 +                res_ids = self.search(cr, uid, [('id', 'not in', mail_ids)], context=context)
 +            res.append(('id', 'in', res_ids))
 +        return res
 +
      _columns = {
 -        'message_is_follower': fields.function(_get_followers,
 -            type='boolean', string='Is a Follower', multi='_get_followers,'),
 +        'message_is_follower': fields.function(_get_followers, type='boolean',
 +            fnct_search=_search_is_follower, string='Is a Follower', multi='_get_followers,'),
          'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
 -                fnct_search=_search_followers, type='many2many', priority=-10,
 -                obj='res.partner', string='Followers', multi='_get_followers'),
 +            fnct_search=_search_followers, type='many2many', priority=-10,
 +            obj='res.partner', string='Followers', multi='_get_followers'),
          'message_ids': fields.one2many('mail.message', 'res_id',
              domain=lambda self: [('model', '=', self._name)],
              auto_join=True,
              context = {}
          if isinstance(ids, (int, long)):
              ids = [ids]
 -
          # Track initial values of tracked fields
          track_ctx = dict(context)
          if 'lang' not in track_ctx:
              track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang
          tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
          if tracked_fields:
 -            initial = self.read(cr, uid, ids, tracked_fields.keys(), context=track_ctx)
 -            initial_values = dict((item['id'], item) for item in initial)
 +            records = self.browse(cr, uid, ids, context=track_ctx)
 +            initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)
  
          # Perform write, update followers
          result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
              if not value:
                  return ''
              if col_info['type'] == 'many2one':
 -                return value[1]
 +                return value.name_get()[0][1]
              if col_info['type'] == 'selection':
                  return dict(col_info['selection'])[value]
              return value
          if not tracked_fields:
              return True
  
 -        for record in self.read(cr, uid, ids, tracked_fields.keys(), context=context):
 -            initial = initial_values[record['id']]
 -            changes = []
 +        for browse_record in self.browse(cr, uid, ids, context=context):
 +            initial = initial_values[browse_record.id]
 +            changes = set()
              tracked_values = {}
  
              # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
              for col_name, col_info in tracked_fields.items():
 -                if record[col_name] == initial[col_name] and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
 +                initial_value = initial[col_name]
 +                record_value = getattr(browse_record, col_name)
 +
 +                if record_value == initial_value and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
                      tracked_values[col_name] = dict(col_info=col_info['string'],
 -                                                        new_value=convert_for_display(record[col_name], col_info))
 -                elif record[col_name] != initial[col_name]:
 +                                                        new_value=convert_for_display(record_value, col_info))
 +                elif record_value != initial_value and (record_value or initial_value):  # because browse null != False
                      if getattr(self._all_columns[col_name].column, 'track_visibility', None) in ['always', 'onchange']:
                          tracked_values[col_name] = dict(col_info=col_info['string'],
 -                                                            old_value=convert_for_display(initial[col_name], col_info),
 -                                                            new_value=convert_for_display(record[col_name], col_info))
 +                                                            old_value=convert_for_display(initial_value, col_info),
 +                                                            new_value=convert_for_display(record_value, col_info))
                      if col_name in tracked_fields:
 -                        changes.append(col_name)
 +                        changes.add(col_name)
              if not changes:
                  continue
  
                  if field not in changes:
                      continue
                  for subtype, method in track_info.items():
 -                    if method(self, cr, uid, record, context):
 +                    if method(self, cr, uid, browse_record, context):
                          subtypes.append(subtype)
  
              posted = False
                      _logger.debug('subtype %s not found, giving error "%s"' % (subtype, e))
                      continue
                  message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
 -                self.message_post(cr, uid, record['id'], body=message, subtype=subtype, context=context)
 +                self.message_post(cr, uid, browse_record.id, body=message, subtype=subtype, context=context)
                  posted = True
              if not posted:
                  message = format_message('', tracked_values)
 -                self.message_post(cr, uid, record['id'], body=message, context=context)
 +                self.message_post(cr, uid, browse_record.id, body=message, context=context)
          return True
  
      #------------------------------------------------------
          ir_attachment_obj.unlink(cr, uid, attach_ids, context=context)
          return True
  
 +    def check_mail_message_access(self, cr, uid, mids, operation, model_obj=None, context=None):
 +        """ mail.message check permission rules for related document. This method is
 +            meant to be inherited in order to implement addons-specific behavior.
 +            A common behavior would be to allow creating messages when having read
 +            access rule on the document, for portal document such as issues. """
 +        if not model_obj:
 +            model_obj = self
 +        if hasattr(self, '_mail_post_access'):
 +            create_allow = self._mail_post_access
 +        else:
 +            create_allow = 'write'
 +
 +        if operation in ['write', 'unlink']:
 +            check_operation = 'write'
 +        elif operation == 'create' and create_allow in ['create', 'read', 'write', 'unlink']:
 +            check_operation = create_allow
 +        elif operation == 'create':
 +            check_operation = 'write'
 +        else:
 +            check_operation = operation
 +
 +        model_obj.check_access_rights(cr, uid, check_operation)
 +        model_obj.check_access_rule(cr, uid, mids, check_operation, context=context)
 +
 +    def _get_formview_action(self, cr, uid, id, model=None, context=None):
 +        """ Return an action to open the document. This method is meant to be
 +            overridden in addons that want to give specific view ids for example.
 +
 +            :param int id: id of the document to open
 +            :param string model: specific model that overrides self._name
 +        """
 +        return {
 +                'type': 'ir.actions.act_window',
 +                'res_model': model or self._name,
 +                'view_type': 'form',
 +                'view_mode': 'form',
 +                'views': [(False, 'form')],
 +                'target': 'current',
 +                'res_id': id,
 +            }
 +
 +    def _get_inbox_action_xml_id(self, cr, uid, context=None):
 +        """ When redirecting towards the Inbox, choose which action xml_id has
 +            to be fetched. This method is meant to be inherited, at least in portal
 +            because portal users have a different Inbox action than classic users. """
 +        return ('mail', 'action_mail_inbox_feeds')
 +
 +    def message_redirect_action(self, cr, uid, context=None):
 +        """ For a given message, return an action that either
 +            - opens the form view of the related document if model, res_id, and
 +              read access to the document
 +            - opens the Inbox with a default search on the conversation if model,
 +              res_id
 +            - opens the Inbox with context propagated
 +
 +        """
 +        if context is None:
 +            context = {}
 +
 +        # default action is the Inbox action
 +        self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
 +        act_model, act_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, *self._get_inbox_action_xml_id(cr, uid, context=context))
 +        action = self.pool.get(act_model).read(cr, uid, act_id, [])
 +        params = context.get('params')
 +        msg_id = model = res_id = None
 +
 +        if params:
 +            msg_id = params.get('message_id')
 +            model = params.get('model')
 +            res_id = params.get('res_id')
 +        if not msg_id and not (model and res_id):
 +            return action
 +        if msg_id and not (model and res_id):
 +            msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
 +            if msg.exists():
 +                model, res_id = msg.model, msg.res_id
 +
 +        # if model + res_id found: try to redirect to the document or fallback on the Inbox
 +        if model and res_id:
 +            model_obj = self.pool.get(model)
 +            if model_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
 +                try:
 +                    model_obj.check_access_rule(cr, uid, [res_id], 'read', context=context)
 +                    if not hasattr(model_obj, '_get_formview_action'):
 +                        action = self.pool.get('mail.thread')._get_formview_action(cr, uid, res_id, model=model, context=context)
 +                    else:
 +                        action = model_obj._get_formview_action(cr, uid, res_id, context=context)
 +                except (osv.except_osv, orm.except_orm):
 +                    pass
 +            action.update({
 +                'context': {
 +                    'search_default_model': model,
 +                    'search_default_res_id': res_id,
 +                }
 +            })
 +        return action
 +
      #------------------------------------------------------
      # Email specific
      #------------------------------------------------------
  
      def message_get_reply_to(self, cr, uid, ids, context=None):
 +        """ Returns the preferred reply-to email address that is basically
 +            the alias of the document, if it exists. """
          if not self._inherits.get('mail.alias'):
              return [False for id in ids]
          return ["%s@%s" % (record['alias_name'], record['alias_domain'])
          """ Used by the plugin addon, based for plugin_outlook and others. """
          ret_dict = {}
          for model_name in self.pool.obj_list():
 -            model = self.pool.get(model_name)
 +            model = self.pool[model_name]
              if hasattr(model, "message_process") and hasattr(model, "message_post"):
                  ret_dict[model_name] = model._description
          return ret_dict
      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.
               4. If all the above fails, raise an exception.
  
             :param string message: an email.message instance
 +           :param dict message_dict: dictionary holding message variables
             :param string model: the fallback model to use if the message
                 does not match any of the currently configured mail aliases
                 (may be None if a matching alias is supposed to be present)
             :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')
          ref_match = thread_references and tools.reference_re.search(thread_references)
          if ref_match:
              thread_id = int(ref_match.group(1))
 -            model = ref_match.group(2) or model
 -            model_pool = self.pool.get(model)
 -            if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
 -                and hasattr(model_pool, 'message_update'):
 -                _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply 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)]
 -
 -        # Verify whether this is a reply to a private message
 +            model = ref_match.group(2) or fallback_model
 +            if thread_id and model in self.pool:
 +                model_obj = self.pool[model]
 +                if model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
 +                    _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
 +                                    email_from, email_to, message_id, model, thread_id, custom_values, uid)
 +                    route = self.message_route_verify(cr, uid, message, message_dict,
 +                                    (model, thread_id, custom_values, uid, None),
 +                                    update_author=True, assert_model=True, create_fallback=True, context=context)
 +                    return route and [route] or []
 +
 +        # 2. Reply to a private message
          if in_reply_to:
 -            message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context)
 -            if message_ids:
 -                message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
 +            mail_message_ids = self.pool.get('mail.message').search(cr, uid, [
 +                                ('message_id', '=', in_reply_to),
 +                                '!', ('message_id', 'ilike', 'reply_to')
 +                            ], limit=1, context=context)
 +            if mail_message_ids:
 +                mail_message = self.pool.get('mail.message').browse(cr, uid, mail_message_ids[0], context=context)
                  _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
 -                                email_from, email_to, message_id, message.id, custom_values, uid)
 -                return [(message.model, message.res_id, custom_values, uid)]
 +                                email_from, email_to, message_id, mail_message.id, custom_values, uid)
 +                route = self.message_route_verify(cr, uid, message, message_dict,
 +                                (mail_message.model, mail_message.res_id, custom_values, uid, None),
 +                                update_author=True, assert_model=True, create_fallback=True, context=context)
 +                return route and [route] or []
  
 -        # 2. Look for a matching mail.alias entry
 +        # 3. Look for a matching mail.alias entry
          # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
          # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
          rcpt_tos = \
                          # user_id = self._message_find_user_id(cr, uid, message, context=context)
                          user_id = uid
                          _logger.info('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.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
 -                                email_from, email_to, message_id, routes)
 +                    route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)
 +                    _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
 +                                email_from, email_to, message_id, route)
 +                    route = self.message_route_verify(cr, uid, message, message_dict, route,
 +                                update_author=True, assert_model=True, create_fallback=True, context=context)
 +                    if route:
 +                        routes.append(route)
                  return routes
  
 -        # 3. Fallback to the provided parameters, if they work
 -        model_pool = self.pool.get(model)
 +        # 4. Fallback to the provided parameters, if they work
          if not thread_id:
              # Legacy: fallback to matching [ID] in the Subject
              match = tools.res_re.search(decode_header(message, 'Subject'))
                  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,
                 to which this mail should be attached. When provided, this
                 overrides the automatic detection based on the message
                 headers.
 -
 -        :raises: ValueError, TypeError
          """
          if context is None:
              context = {}
          msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
          if strip_attachments:
              msg.pop('attachments', None)
 +
          if msg.get('message_id'):   # should always be True as message_parse generate one if missing
              existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [
                                                                  ('message_id', '=', msg.get('message_id')),
                                                                  ], context=context)
              if existing_msg_ids:
 -                _logger.info('Ignored mail from %s to %s with Message-Id %s:: found duplicated Message-Id during processing',
 +                _logger.info('Ignored mail from %s to %s with Message-Id %s: found duplicated Message-Id during processing',
                                  msg.get('from'), msg.get('to'), msg.get('message_id'))
                  return False
  
          # find possible routes for the message
 -        routes = self.message_route(cr, uid, msg_txt, model,
 -                                    thread_id, custom_values,
 -                                    context=context)
 -
 -        # postpone setting msg.partner_ids after message_post, to avoid double notifications
 -        partner_ids = msg.pop('partner_ids', [])
 -
 -        thread_id = False
 -        for model, thread_id, custom_values, user_id in routes:
 -            if self._name == 'mail.thread':
 -                context.update({'thread_model': model})
 -            if model:
 -                model_pool = self.pool.get(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" %
 -                        (msg['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)
 -                if thread_id and hasattr(model_pool, 'message_update'):
 -                    model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
 -                else:
 -                    nosub_ctx = dict(nosub_ctx, mail_create_nolog=True)
 -                    thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
 -            else:
 -                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')
 -            new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
 -
 -            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)
 -
 +        routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context)
 +        thread_id = self.message_route_process(cr, uid, msg_txt, msg, routes, context=context)
          return thread_id
  
      def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
          if isinstance(custom_values, dict):
              data = custom_values.copy()
          model = context.get('thread_model') or self._name
 -        model_pool = self.pool.get(model)
 +        model_pool = self.pool[model]
          fields = model_pool.fields_get(cr, uid, context=context)
          if 'name' in fields and not data.get('name'):
              data['name'] = msg_dict.get('subject', '')
          """
          msg_dict = {
              'type': 'email',
 -            'author_id': False,
          }
          if not isinstance(message, Message):
              if isinstance(message, unicode):
          msg_dict['from'] = decode(message.get('from'))
          msg_dict['to'] = decode(message.get('to'))
          msg_dict['cc'] = decode(message.get('cc'))
 -
 -        if message.get('From'):
 -            author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
 -            if author_ids:
 -                msg_dict['author_id'] = author_ids[0]
 -            msg_dict['email_from'] = decode(message.get('from'))
 +        msg_dict['email_from'] = decode(message.get('from'))
          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]
  
                  partner_id, partner_name<partner_email> or partner_name, reason """
          if email and not partner:
              # get partner info from email
 -            partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context, res_id=obj.id)
 -            if partner_info and partner_info[0].get('partner_id'):
 -                partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info[0]['partner_id']], context=context)[0]
 +            partner_info = self.message_partner_info_from_emails(cr, uid, obj.id, [email], context=context)[0]
 +            if partner_info.get('partner_id'):
 +                partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info['partner_id']], context=context)[0]
          if email and email in [val[1] for val in result[obj.id]]:  # already existing email -> skip
              return result
          if partner and partner in obj.message_follower_ids:  # recipient already in the followers -> skip
                  self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
          return result
  
 -    def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None, res_id=None):
 -        """ Wrapper with weird order parameter because of 7.0 fix.
 +    def _find_partner_from_emails(self, cr, uid, id, emails, model=None, context=None, check_followers=True):
 +        """ Utility method to find partners from email addresses. The rules are :
 +            1 - check in document (model | self, id) followers
 +            2 - try to find a matching partner that is also an user
 +            3 - try to find a matching partner
  
 -            TDE TODO: remove me in 8.0 """
 -        return self.message_find_partner_from_emails(cr, uid, res_id, emails, link_mail=link_mail, context=context)
 +            :param list emails: list of email addresses
 +            :param string model: model to fetch related record; by default self
 +                is used.
 +            :param boolean check_followers: check in document followers
 +        """
 +        partner_obj = self.pool['res.partner']
 +        partner_ids = []
 +        obj = None
 +        if id and (model or self._name != 'mail.thread') and check_followers:
 +            if model:
 +                obj = self.pool[model].browse(cr, uid, id, context=context)
 +            else:
 +                obj = self.browse(cr, uid, id, context=context)
 +        for contact in emails:
 +            partner_id = False
 +            email_address = tools.email_split(contact)
 +            if not email_address:
 +                partner_ids.append(partner_id)
 +                continue
 +            email_address = email_address[0]
 +            # first try: check in document's followers
 +            if obj:
 +                for follower in obj.message_follower_ids:
 +                    if follower.email == email_address:
 +                        partner_id = follower.id
 +            # second try: check in partners that are also users
 +            if not partner_id:
 +                ids = partner_obj.search(cr, SUPERUSER_ID, [
 +                                                ('email', 'ilike', email_address),
 +                                                ('user_ids', '!=', False)
 +                                            ], limit=1, context=context)
 +                if ids:
 +                    partner_id = ids[0]
 +            # third try: check in partners
 +            if not partner_id:
 +                ids = partner_obj.search(cr, SUPERUSER_ID, [
 +                                                ('email', 'ilike', email_address)
 +                                            ], limit=1, context=context)
 +                if ids:
 +                    partner_id = ids[0]
 +            partner_ids.append(partner_id)
 +        return partner_ids
  
 -    def message_find_partner_from_emails(self, cr, uid, id, emails, link_mail=False, context=None):
 +    def message_partner_info_from_emails(self, cr, uid, id, emails, link_mail=False, 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
 -
 -            TDE TODO: merge me with other partner finding methods in 8.0 """
 +            :return dict: partner_ids and new_partner_ids """
          mail_message_obj = self.pool.get('mail.message')
 -        partner_obj = self.pool.get('res.partner')
 +        partner_ids = self._find_partner_from_emails(cr, uid, id, emails, context=context)
          result = list()
 -        if id and self._name != 'mail.thread':
 -            obj = self.browse(cr, SUPERUSER_ID, id, context=context)
 -        else:
 -            obj = None
 -        for email in emails:
 -            partner_info = {'full_name': email, 'partner_id': False}
 -            m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
 -            if not m:
 -                continue
 -            email_address = m.group(3)
 -            # first try: check in document's followers
 -            if obj:
 -                for follower in obj.message_follower_ids:
 -                    if follower.email == email_address:
 -                        partner_info['partner_id'] = follower.id
 -            # second try: check in partners
 -            if not partner_info.get('partner_id'):
 -                ids = partner_obj.search(cr, SUPERUSER_ID, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context)
 -                if not ids:
 -                    ids = partner_obj.search(cr, SUPERUSER_ID, [('email', 'ilike', email_address)], limit=1, context=context)
 -                if ids:
 -                    partner_info['partner_id'] = ids[0]
 +        for idx in range(len(emails)):
 +            email_address = emails[idx]
 +            partner_id = partner_ids[idx]
 +            partner_info = {'full_name': email_address, 'partner_id': partner_id}
              result.append(partner_info)
  
              # link mail with this from mail to the new partner id
              if link_mail and partner_info['partner_id']:
                  message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
                                      '|',
 -                                    ('email_from', '=', email),
 -                                    ('email_from', 'ilike', '<%s>' % email),
 +                                    ('email_from', '=', email_address),
 +                                    ('email_from', 'ilike', '<%s>' % email_address),
                                      ('author_id', '=', False)
                                  ], context=context)
                  if message_ids:
                      mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': partner_info['partner_id']}, context=context)
          return result
  
 +    def _message_preprocess_attachments(self, cr, uid, attachments, attachment_ids, attach_model, attach_res_id, context=None):
 +        """ Preprocess attachments for mail_thread.message_post() or mail_mail.create().
 +
 +        :param list attachments: list of attachment tuples in the form ``(name,content)``,
 +                                 where content is NOT base64 encoded
 +        :param list attachment_ids: a list of attachment ids, not in tomany command form
 +        :param str attach_model: the model of the attachments parent record
 +        :param integer attach_res_id: the id of the attachments parent record
 +        """
 +        Attachment = self.pool['ir.attachment']
 +        m2m_attachment_ids = []
 +        if attachment_ids:
 +            filtered_attachment_ids = Attachment.search(cr, SUPERUSER_ID, [
 +                ('res_model', '=', 'mail.compose.message'),
 +                ('create_uid', '=', uid),
 +                ('id', 'in', attachment_ids)], context=context)
 +            if filtered_attachment_ids:
 +                Attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': attach_model, 'res_id': attach_res_id}, context=context)
 +            m2m_attachment_ids += [(4, id) for id in attachment_ids]
 +        # Handle attachments parameter, that is a dictionary of attachments
 +        for name, content in attachments:
 +            if isinstance(content, unicode):
 +                content = content.encode('utf-8')
 +            data_attach = {
 +                'name': name,
 +                'datas': base64.b64encode(str(content)),
 +                'datas_fname': name,
 +                'description': name,
 +                'res_model': attach_model,
 +                'res_id': attach_res_id,
 +            }
 +            m2m_attachment_ids.append((0, 0, data_attach))
 +        return m2m_attachment_ids
 +
      def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
 -                        subtype=None, parent_id=False, attachments=None, context=None,
 -                        content_subtype='html', **kwargs):
 +                     subtype=None, parent_id=False, attachments=None, context=None,
 +                     content_subtype='html', **kwargs):
          """ Post a new message in an existing thread, returning the new
              mail.message ID.
  
          model = False
          if thread_id:
              model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
 -            if model != self._name:
 +            if model != self._name and hasattr(self.pool[model], 'message_post'):
                  del context['thread_model']
 -                return self.pool.get(model).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
 -
 -        # 0: Parse email-from, try to find a better author_id based on document's followers for incoming emails
 -        email_from = kwargs.get('email_from')
 -        if email_from and thread_id and type == 'email' and kwargs.get('author_id'):
 -            email_list = tools.email_split(email_from)
 -            doc = self.browse(cr, uid, thread_id, context=context)
 -            if email_list and doc:
 -                author_ids = self.pool.get('res.partner').search(cr, uid, [
 -                                        ('email', 'ilike', email_list[0]),
 -                                        ('id', 'in', [f.id for f in doc.message_follower_ids])
 -                                    ], limit=1, context=context)
 -                if author_ids:
 -                    kwargs['author_id'] = author_ids[0]
 +                return self.pool[model].message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
 +
 +        #0: Find the message's author, because we need it for private discussion
          author_id = kwargs.get('author_id')
          if author_id is None:  # keep False values
              author_id = self.pool.get('mail.message')._get_default_author(cr, uid, context=context)
  
          # 3. Attachments
          #   - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
 -        attachment_ids = kwargs.pop('attachment_ids', []) or []  # because we could receive None (some old code sends None)
 -        if attachment_ids:
 -            filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
 -                ('res_model', '=', 'mail.compose.message'),
 -                ('create_uid', '=', uid),
 -                ('id', 'in', attachment_ids)], context=context)
 -            if filtered_attachment_ids:
 -                ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
 -        attachment_ids = [(4, id) for id in attachment_ids]
 -        # Handle attachments parameter, that is a dictionary of attachments
 -        for name, content in attachments:
 -            if isinstance(content, unicode):
 -                content = content.encode('utf-8')
 -            data_attach = {
 -                'name': name,
 -                'datas': base64.b64encode(str(content)),
 -                'datas_fname': name,
 -                'description': name,
 -                'res_model': model,
 -                'res_id': thread_id,
 -            }
 -            attachment_ids.append((0, 0, data_attach))
 +        attachment_ids = self._message_preprocess_attachments(cr, uid, attachments, kwargs.pop('attachment_ids', []), model, thread_id, context)
  
          # 4: mail.message.subtype
          subtype_id = False
          return msg_id
  
      #------------------------------------------------------
      # Followers API
      #------------------------------------------------------
  
 -    def message_get_subscription_data(self, cr, uid, ids, context=None):
 +    def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
          """ Wrapper to get subtypes data. """
 -        return self._get_subscription_data(cr, uid, ids, None, None, context=context)
 +        return self._get_subscription_data(cr, uid, ids, None, None, user_pid=user_pid, context=context)
  
      def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
          """ Wrapper on message_subscribe, using users. If user_ids is not
          ''', (ids, self._name, partner_id))
          return True
  
 -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
 +    #------------------------------------------------------
 +    # Thread suggestion
 +    #------------------------------------------------------
 +
 +    def get_suggested_thread(self, cr, uid, removed_suggested_threads=None, context=None):
 +        """Return a list of suggested threads, sorted by the numbers of followers"""
 +        if context is None:
 +            context = {}
 +
 +        # TDE HACK: originally by MAT from portal/mail_mail.py but not working until the inheritance graph bug is not solved in trunk
 +        # TDE FIXME: relocate in portal when it won't be necessary to reload the hr.employee model in an additional bridge module
 +        if self.pool['res.groups']._all_columns.get('is_portal'):
 +            user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
 +            if any(group.is_portal for group in user.groups_id):
 +                return []
 +
 +        threads = []
 +        if removed_suggested_threads is None:
 +            removed_suggested_threads = []
 +
 +        thread_ids = self.search(cr, uid, [('id', 'not in', removed_suggested_threads), ('message_is_follower', '=', False)], context=context)
 +        for thread in self.browse(cr, uid, thread_ids, context=context):
 +            data = {
 +                'id': thread.id,
 +                'popularity': len(thread.message_follower_ids),
 +                'name': thread.name,
 +                'image_small': thread.image_small
 +            }
 +            threads.append(data)
 +        return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]
      width: 32px;
      height: 32px;
      padding: 0px;
 -    margin: 0px
 +    margin: 0px;
      border-radius: 0px;
  }
  
 +/* ---- GENERIC FOR MAIL-RELATED STUFF ---- */
 +.openerp .oe_e.oe_e_alias {
 +  font-size: 30px;
 +  line-height: 15px;
 +  vertical-align: top;
 +  margin-right: 3px;
 +  color: white;
 +  text-shadow: 0px 0px 2px black;
 +}
 +
  /* ------------ MAIL WIDGET --------------- */
  .openerp .oe_mail, .openerp .oe_mail *{
      -webkit-box-sizing: border-box;
@@@ -53,9 -43,6 +53,9 @@@
  .openerp .oe_mail > .oe_thread{
      margin-left: 0px;
  }
 +.openerp .oe_inline.oe_compose_recipients {
 +    margin-top: -2px;
 +}
  
  /* ---------------- MESSAGES ------------------ */
  
  .openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p {
      margin-bottom: 0px;
  }
+ .openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body pre {
+     white-space: pre-wrap;
+ }
  .openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body * {
      text-overflow:ellipsis;
      word-wrap: break-word;
  }
 +.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body .oe_mail_cleaned {
 +    display: none;
 +}
  
  /* a) Indented Messages */
  
  }
  .openerp .oe_followers .oe_partner {
      height: 32px;
 +    margin-right: 24px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
 -    margin-right: 10px;
  }
  .openerp .oe_followers .oe_partner img{
      width: 32px;
      right: 0px;
      line-height: 20px;
  }
 -
 +.openerp .oe_followers .oe_edit_subtype{
 +    cursor: pointer;
 +    position: absolute;
 +    right: 12px;
 +    line-height: 20px;
 +}
 +.openerp .oe_followers .oe_partner .oe_hidden{
 +    display: none;
 +}
 +.openerp.ui-dialog .ui-dialog-titlebar .ui-dialog-title{
 +    padding-right: 20px;
 +}
  .openerp .oe_followers .oe_show_more{
      cursor: pointer;
  }
  .openerp .oe_mail_wall .oe_mail{
      margin: 16px;
      width: 600px;
 +    display: inline-block;
  }
  
  .openerp .oe_mail .oe_view_nocontent > p {
      padding-left: 15px;
  }
 +
 +
 +/* ------------- WALL SIDEBAR ------------- */
 +
 +.openerp .oe_mail_wall .oe_mail_wall_aside {
 +    margin: 16px;
 +    position: relative;
 +    display: inline-block;
 +    vertical-align: top;
 +    width: 260px;
 +}
 +.openerp .oe_mail_wall_aside .oe_sidebar_suggestion {
 +    background-color: #EDEDF6;
 +    border-radius: 2px;
 +    padding-top: 1px;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_title h2 {
 +    font-size: 14px;
 +    font-weight: bold;
 +    margin-left: 10px;
 +    padding: 0px;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item {
 +    border-radius: 2px;
 +    width: 100%;
 +    margin-left: 10px;
 +    min-height: 67px;  /* image_small 66x66px */
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_image {
 +    float: left;
 +    padding-right: 10px;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_image img {
 +    border-radius: 2px;
 +    border: solid 1px rgba(0,0,0,0.03);
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content button {
 +    margin-top: 10px;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content a.oe_suggestion_item_name {
 +    text-overflow: ellipsis;
 +    overflow: hidden;
 +    width: 90%;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_title a.oe_suggestion_remove {
 +    line-height: 15px;
 +    margin-top: -2px;
 +    float: right;
 +    visibility: hidden;
 +    margin-right: 7px;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content a.oe_suggestion_remove_item {
 +    line-height: 15px;
 +    margin-top: -2px;
 +    float: right;
 +    visibility: hidden;
 +    margin-right: 16px;
 +}
 +.openerp .oe_sidebar_suggestion .oe_suggest_title:hover a.oe_suggestion_remove,
 +.openerp .oe_sidebar_suggestion .oe_suggest_items:hover a.oe_suggestion_remove_item {
 +    visibility: visible;
 +}
 +
@@@ -19,8 -19,7 +19,8 @@@
  #
  ##############################################################################
  
 -from openerp.addons.mail.tests.test_mail_base import TestMailBase
 +from openerp.addons.mail.tests.common import TestMail
 +from openerp.tools import mute_logger
  
  MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
  To: {to}
@@@ -143,9 -142,49 +143,9 @@@ dGVzdAo
  --089e01536c4ed4d17204e49b8e96--"""
  
  
 -class TestMailgateway(TestMailBase):
 +class TestMailgateway(TestMail):
  
 -    def test_00_partner_find_from_email(self):
 -        """ Tests designed for partner fetch based on emails. """
 -        cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
 -
 -        # --------------------------------------------------
 -        # Data creation
 -        # --------------------------------------------------
 -        # 1 - Partner ARaoul
 -        p_a_id = self.res_partner.create(cr, uid, {'name': 'ARaoul', 'email': 'test@test.fr'})
 -
 -        # --------------------------------------------------
 -        # CASE1: without object
 -        # --------------------------------------------------
 -
 -        # Do: find partner with email -> first partner should be found
 -        partner_info = self.mail_thread.message_find_partner_from_emails(cr, uid, None, ['Maybe Raoul <test@test.fr>'], link_mail=False)[0]
 -        self.assertEqual(partner_info['full_name'], 'Maybe Raoul <test@test.fr>',
 -                        'mail_thread: message_find_partner_from_emails did not handle email')
 -        self.assertEqual(partner_info['partner_id'], p_a_id,
 -                        'mail_thread: message_find_partner_from_emails wrong partner found')
 -
 -        # Data: add some data about partners
 -        # 2 - User BRaoul
 -        p_b_id = self.res_partner.create(cr, uid, {'name': 'BRaoul', 'email': 'test@test.fr', 'user_ids': [(4, user_raoul.id)]})
 -
 -        # Do: find partner with email -> first user should be found
 -        partner_info = self.mail_thread.message_find_partner_from_emails(cr, uid, None, ['Maybe Raoul <test@test.fr>'], link_mail=False)[0]
 -        self.assertEqual(partner_info['partner_id'], p_b_id,
 -                        'mail_thread: message_find_partner_from_emails wrong partner found')
 -
 -        # --------------------------------------------------
 -        # CASE1: with object
 -        # --------------------------------------------------
 -
 -        # Do: find partner in group where there is a follower with the email -> should be taken
 -        self.mail_group.message_subscribe(cr, uid, [group_pigs.id], [p_b_id])
 -        partner_info = self.mail_group.message_find_partner_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul <test@test.fr>'], link_mail=False)[0]
 -        self.assertEqual(partner_info['partner_id'], p_b_id,
 -                        'mail_thread: message_find_partner_from_emails wrong partner found')
 -
 -    def test_09_message_parse(self):
 +    def test_00_message_parse(self):
          """ Testing incoming emails parsing """
          cr, uid = self.cr, self.uid
  
          self.assertIn('<div dir="ltr">Should create a multipart/mixed: from gmail, <b>bold</b>, with attachment.<br clear="all"><div><br></div>', res.get('body', ''),
                        'message_parse: html version should be in body after parsing multipart/mixed')
  
 +    @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
      def test_10_message_process(self):
          """ Testing incoming emails processing. """
          cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
  
          def format_and_process(template, to='groups@example.com, other@gmail.com', subject='Frogs',
 -                                extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
 -                                msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
 -                                model=None):
 +                               extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
 +                               msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
 +                               model=None):
              self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', subject)]), [])
              mail = template.format(to=to, subject=subject, extra=extra, email_from=email_from, msg_id=msg_id)
              self.mail_thread.message_process(cr, uid, model, mail)
          alias_id = self.mail_alias.create(cr, uid, {
              'alias_name': 'groups',
              'alias_user_id': False,
 -            'alias_model_id': self.mail_group_model_id})
 +            'alias_model_id': self.mail_group_model_id,
 +            'alias_parent_model_id': self.mail_group_model_id,
 +            'alias_parent_thread_id': self.group_pigs_id,
 +            'alias_contact': 'everyone'})
  
          # --------------------------------------------------
          # Test1: new record creation
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          res = self.mail_group.perm_read(cr, uid, [frog_group.id], details=False)
          self.assertEqual(res[0].get('create_uid'), uid,
 -                            'message_process: group should have been created by uid as alias_user__id is False on the alias')
 +                         'message_process: group should have been created by uid as alias_user__id is False on the alias')
          # Test: one message that is the incoming email
          self.assertEqual(len(frog_group.message_ids), 1,
 -                            'message_process: newly created group should have the incoming email in message_ids')
 +                         'message_process: newly created group should have the incoming email in message_ids')
          msg = frog_group.message_ids[0]
          self.assertEqual('Frogs', msg.subject,
 -                            'message_process: newly created group should have the incoming email as first message')
 +                         'message_process: newly created group should have the incoming email as first message')
          self.assertIn('Please call me as soon as possible this afternoon!', msg.body,
 -                            'message_process: newly created group should have the incoming email as first message')
 +                      'message_process: newly created group should have the incoming email as first message')
          self.assertEqual('email', msg.type,
 -                            'message_process: newly created group should have an email as first message')
 +                         'message_process: newly created group should have an email as first message')
          self.assertEqual('Discussions', msg.subtype_id.name,
 -                            'message_process: newly created group should not have a log first message but an email')
 +                         'message_process: newly created group should not have a log first message but an email')
          # Test: message: unknown email address -> message has email_from, not author_id
          self.assertFalse(msg.author_id,
 -                            'message_process: message on created group should not have an author_id')
 +                         'message_process: message on created group should not have an author_id')
          self.assertIn('test.sylvie.lelitre@agrolait.com', msg.email_from,
 -                            'message_process: message on created group should have an email_from')
 +                      'message_process: message on created group should have an email_from')
          # Test: followers: nobody
          self.assertEqual(len(frog_group.message_follower_ids), 0, 'message_process: newly create group should not have any follower')
          # Test: sent emails: no-one
          self.assertEqual(len(sent_emails), 0,
 -                            'message_process: should create emails without any follower added')
 +                         'message_process: should create emails without any follower added')
          # Data: unlink group
          frog_group.unlink()
  
 -        # Do: incoming email from a known partner on an alias with known recipients, alias is owned by user that can create a group
 -        self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': self.user_raoul_id})
 +        # Do: incoming email from an unknown partner on a Partners only alias -> bounce
 +        self._init_mock_build_email()
 +        self.mail_alias.write(cr, uid, [alias_id], {'alias_contact': 'partners'})
 +        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other2@gmail.com')
 +        # Test: no group created
 +        self.assertTrue(len(frog_groups) == 0)
 +        # Test: email bounced
 +        sent_emails = self._build_email_kwargs_list
 +        self.assertEqual(len(sent_emails), 1,
 +                         'message_process: incoming email on Partners alias should send a bounce email')
 +        self.assertIn('Frogs', sent_emails[0].get('subject'),
 +                      'message_process: bounce email on Partners alias should contain the original subject')
 +        self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to'),
 +                      'message_process: bounce email on Partners alias should have original email sender as recipient')
 +
 +        # Do: incoming email from an unknown partner on a Followers only alias -> bounce
 +        self._init_mock_build_email()
 +        self.mail_alias.write(cr, uid, [alias_id], {'alias_contact': 'followers'})
 +        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other3@gmail.com')
 +        # Test: no group created
 +        self.assertTrue(len(frog_groups) == 0)
 +        # Test: email bounced
 +        sent_emails = self._build_email_kwargs_list
 +        self.assertEqual(len(sent_emails), 1,
 +                         'message_process: incoming email on Followers alias should send a bounce email')
 +        self.assertIn('Frogs', sent_emails[0].get('subject'),
 +                      'message_process: bounce email on Followers alias should contain the original subject')
 +        self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to'),
 +                      'message_process: bounce email on Followers alias should have original email sender as recipient')
 +
 +        # Do: incoming email from a known partner on a Partners alias -> ok (+ test on alias.user_id)
 +        self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': self.user_raoul_id, 'alias_contact': 'partners'})
          p1id = self.res_partner.create(cr, uid, {'name': 'Sylvie Lelitre', 'email': 'test.sylvie.lelitre@agrolait.com'})
 -        p2id = self.res_partner.create(cr, uid, {'name': 'Other Poilvache', 'email': 'other@gmail.com'})
 +        p2id = self.res_partner.create(cr, uid, {'name': 'Other Poilvache', 'email': 'other4@gmail.com'})
          self._init_mock_build_email()
 -        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com')
 +        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other4@gmail.com')
          sent_emails = self._build_email_kwargs_list
          # Test: one group created by Raoul
          self.assertEqual(len(frog_groups), 1, 'message_process: a new mail.group should have been created')
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          res = self.mail_group.perm_read(cr, uid, [frog_group.id], details=False)
          self.assertEqual(res[0].get('create_uid'), self.user_raoul_id,
 -                            'message_process: group should have been created by alias_user_id')
 +                         'message_process: group should have been created by alias_user_id')
          # Test: one message that is the incoming email
          self.assertEqual(len(frog_group.message_ids), 1,
 -                            'message_process: newly created group should have the incoming email in message_ids')
 +                         'message_process: newly created group should have the incoming email in message_ids')
          msg = frog_group.message_ids[0]
 -        # Test: message: unknown email address -> message has email_from, not author_id
 +        # Test: message: author found
          self.assertEqual(p1id, msg.author_id.id,
 -                            'message_process: message on created group should have Sylvie as author_id')
 +                         'message_process: message on created group should have Sylvie as author_id')
          self.assertIn('Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>', msg.email_from,
 -                            'message_process: message on created group should have have an email_from')
 -        # Test: author (not recipient and not raoul (as alias owner)) added as follower
 +                      'message_process: message on created group should have have an email_from')
 +        # Test: author (not recipient and not Raoul (as alias owner)) added as follower
          frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
          self.assertEqual(frog_follower_ids, set([p1id]),
 -                            'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
 +                         'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
          # Test: sent emails: no-one, no bounce effet
 +        sent_emails = self._build_email_kwargs_list
          self.assertEqual(len(sent_emails), 0,
 -                            'message_process: should not bounce incoming emails')
 +                         'message_process: should not bounce incoming emails')
          # Data: unlink group
          frog_group.unlink()
  
 -        # Do: incoming email from a known partner that is also an user that can create a mail.group
 -        self.res_users.create(cr, uid, {'partner_id': p1id, 'login': 'sylvie', 'groups_id': [(6, 0, [self.group_employee_id])]})
 -        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com')
 +        # Do: incoming email from a not follower Partner on a Followers only alias -> bounce
 +        self._init_mock_build_email()
 +        self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': False, 'alias_contact': 'followers'})
 +        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other5@gmail.com')
 +        # Test: no group created
 +        self.assertTrue(len(frog_groups) == 0)
 +        # Test: email bounced
 +        sent_emails = self._build_email_kwargs_list
 +        self.assertEqual(len(sent_emails), 1,
 +                         'message_process: incoming email on Partners alias should send a bounce email')
 +
 +        # Do: incoming email from a parent document follower on a Followers only alias -> ok
 +        self._init_mock_build_email()
 +        self.mail_group.message_subscribe(cr, uid, [self.group_pigs_id], [p1id])
 +        frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other6@gmail.com')
          # Test: one group created by Raoul (or Sylvie maybe, if we implement it)
          self.assertEqual(len(frog_groups), 1, 'message_process: a new mail.group should have been created')
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          # Test: one message that is the incoming email
          self.assertEqual(len(frog_group.message_ids), 1,
 -                            'message_process: newly created group should have the incoming email in message_ids')
 +                         'message_process: newly created group should have the incoming email in message_ids')
          # Test: author (and not recipient) added as follower
          frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
          self.assertEqual(frog_follower_ids, set([p1id]),
 -                            'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
 +                         'message_process: newly created group should have 1 follower (author, not creator, not recipients)')
          # Test: sent emails: no-one, no bounce effet
 +        sent_emails = self._build_email_kwargs_list
          self.assertEqual(len(sent_emails), 0,
 -                            'message_process: should not bounce incoming emails')
 +                         'message_process: should not bounce incoming emails')
  
          # --------------------------------------------------
 -        # Test2: discussion update
 +        # Test2: update-like alias
          # --------------------------------------------------
  
 -        # Do: even with a wrong destination, a reply should end up in the correct thread
 -        frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other@gmail.com',
 -                                            msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
 -                                            to='erroneous@example.com>', subject='Re: news',
 -                                            extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
 +        # Do: Pigs alias is restricted, should bounce
 +        self._init_mock_build_email()
 +        self.mail_group.write(cr, uid, [frog_group.id], {'alias_name': 'frogs', 'alias_contact': 'followers', 'alias_force_thread_id': frog_group.id})
 +        frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
 +                                         msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
 +                                         to='frogs@example.com>', subject='Re: news')
          # Test: no group 'Re: news' created, still only 1 Frogs group
          self.assertEqual(len(frog_groups), 0,
 -                            'message_process: reply on Frogs should not have created a new group with new subject')
 +                         'message_process: reply on Frogs should not have created a new group with new subject')
          frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
          self.assertEqual(len(frog_groups), 1,
 -                            'message_process: reply on Frogs should not have created a duplicate group with old subject')
 +                         'message_process: reply on Frogs should not have created a duplicate group with old subject')
 +        frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
 +        # Test: email bounced
 +        sent_emails = self._build_email_kwargs_list
 +        self.assertEqual(len(sent_emails), 1,
 +                         'message_process: incoming email on Followers alias should send a bounce email')
 +        self.assertIn('Re: news', sent_emails[0].get('subject'),
 +                      'message_process: bounce email on Followers alias should contain the original subject')
 +
 +        # Do: Pigs alias is restricted, should accept Followers
 +        self._init_mock_build_email()
 +        self.mail_group.message_subscribe(cr, uid, [frog_group.id], [p2id])
 +        frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
 +                                         msg_id='<1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>',
 +                                         to='frogs@example.com>', subject='Re: cats')
 +        # Test: no group 'Re: news' created, still only 1 Frogs group
 +        self.assertEqual(len(frog_groups), 0,
 +                         'message_process: reply on Frogs should not have created a new group with new subject')
 +        frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
 +        self.assertEqual(len(frog_groups), 1,
 +                         'message_process: reply on Frogs should not have created a duplicate group with old subject')
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          # Test: one new message
          self.assertEqual(len(frog_group.message_ids), 2, 'message_process: group should contain 2 messages after reply')
 +        # Test: sent emails: 1 (Sylvie copy of the incoming email, but no bounce)
 +        sent_emails = self._build_email_kwargs_list
 +        self.assertEqual(len(sent_emails), 1,
 +                         'message_process: one email should have been generated')
 +        self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to')[0],
 +                      'message_process: email should be sent to Sylvie')
 +        self.mail_group.message_unsubscribe(cr, uid, [frog_group.id], [p2id])
 +
 +        # --------------------------------------------------
 +        # Test3: discussion and replies
 +        # --------------------------------------------------
 +
 +        # Do: even with a wrong destination, a reply should end up in the correct thread
 +        frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
 +                                         msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
 +                                         to='erroneous@example.com>', subject='Re: news',
 +                                         extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
 +        # Test: no group 'Re: news' created, still only 1 Frogs group
 +        self.assertEqual(len(frog_groups), 0,
 +                         'message_process: reply on Frogs should not have created a new group with new subject')
 +        frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
 +        self.assertEqual(len(frog_groups), 1,
 +                         'message_process: reply on Frogs should not have created a duplicate group with old subject')
 +        frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
 +        # Test: one new message
 +        self.assertEqual(len(frog_group.message_ids), 3, 'message_process: group should contain 2 messages after reply')
          # Test: author (and not recipient) added as follower
          frog_follower_ids = set([p.id for p in frog_group.message_follower_ids])
          self.assertEqual(frog_follower_ids, set([p1id, p2id]),
 -                            'message_process: after reply, group should have 2 followers')
 +                         'message_process: after reply, group should have 2 followers')
  
          # Do: due to some issue, same email goes back into the mailgateway
 -        frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other@gmail.com',
 -                                            msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
 -                                            subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
 +        frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com',
 +                                         msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>',
 +                                         subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
          # Test: no group 'Re: news' created, still only 1 Frogs group
          self.assertEqual(len(frog_groups), 0,
 -                            'message_process: reply on Frogs should not have created a new group with new subject')
 +                         'message_process: reply on Frogs should not have created a new group with new subject')
          frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
          self.assertEqual(len(frog_groups), 1,
 -                            'message_process: reply on Frogs should not have created a duplicate group with old subject')
 +                         'message_process: reply on Frogs should not have created a duplicate group with old subject')
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          # Test: no new message
 -        self.assertEqual(len(frog_group.message_ids), 2, 'message_process: message with already existing message_id should not have been duplicated')
 +        self.assertEqual(len(frog_group.message_ids), 3, 'message_process: message with already existing message_id should not have been duplicated')
          # Test: message_id is still unique
          msg_ids = self.mail_message.search(cr, uid, [('message_id', 'ilike', '<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>')])
          self.assertEqual(len(msg_ids), 1,
 -                            'message_process: message with already existing message_id should not have been duplicated')
 +                         'message_process: message with already existing message_id should not have been duplicated')
  
          # --------------------------------------------------
 -        # Test3: email_from and partner finding
 +        # Test4: email_from and partner finding
          # --------------------------------------------------
  
          # Data: extra partner with Raoul's email -> test the 'better author finding'
          extra_partner_id = self.res_partner.create(cr, uid, {'name': 'A-Raoul', 'email': 'test_raoul@email.com'})
  
          # Do: post a new message, with a known partner -> duplicate emails -> partner
          format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik <test_raoul@email.com>',
 -                                            to='erroneous@example.com>', subject='Re: news (2)',
 -                                            msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>',
 -                                            extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
 +                           to='erroneous@example.com>', subject='Re: news (2)',
 +                           msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>',
 +                           extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
          frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          # Test: author is A-Raoul (only existing)
          self.assertEqual(frog_group.message_ids[0].author_id.id, extra_partner_id,
 -                            'message_process: email_from -> author_id wrong')
 +                         'message_process: email_from -> author_id wrong')
  
          # Do: post a new message, with a known partner -> duplicate emails -> user
          frog_group.message_unsubscribe([extra_partner_id])
          raoul_email = self.user_raoul.email
          self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'})
          format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik <test_raoul@email.com>',
 -                                            to='erroneous@example.com>', subject='Re: news (3)',
 -                                            msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>',
 -                                            extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
 +                           to='erroneous@example.com>', subject='Re: news (3)',
 +                           msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>',
 +                           extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
          frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          # Test: author is Raoul (user), not A-Raoul
          self.assertEqual(frog_group.message_ids[0].author_id.id, self.partner_raoul_id,
 -                            'message_process: email_from -> author_id wrong')
 +                         'message_process: email_from -> author_id wrong')
  
          # Do: post a new message, with a known partner -> duplicate emails -> partner because is follower
          frog_group.message_unsubscribe([self.partner_raoul_id])
          raoul_email = self.user_raoul.email
          self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'})
          format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik <test_raoul@email.com>',
 -                                            to='erroneous@example.com>', subject='Re: news (3)',
 -                                            msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>',
 -                                            extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
 +                           to='erroneous@example.com>', subject='Re: news (3)',
 +                           msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>',
 +                           extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
          frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')])
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          # Test: author is Raoul (user), not A-Raoul
          self.assertEqual(frog_group.message_ids[0].author_id.id, extra_partner_id,
 -                            'message_process: email_from -> author_id wrong')
 +                         'message_process: email_from -> author_id wrong')
  
          self.res_users.write(cr, uid, self.user_raoul_id, {'email': raoul_email})
  
          # --------------------------------------------------
 -        # Test4: misc gateway features
 +        # Test5: misc gateway features
          # --------------------------------------------------
  
          # 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,
 -                                            to='noone@example.com',
 -                                            subject='Spammy', extra='', model='mail.group',
 -                                            msg_id='<1198923581.41972151344608186760.JavaMail.new6@agrolait.com>')
 +                                         to='noone@example.com',
 +                                         subject='Spammy', extra='', model='mail.group',
 +                                         msg_id='<1198923581.41972151344608186760.JavaMail.new6@agrolait.com>')
          self.assertEqual(len(frog_groups), 1,
 -                            'message_process: erroneous email but with a fallback model should have created a new mail.group')
 +                         'message_process: erroneous email but with a fallback model should have created a new mail.group')
  
          # Do: incoming email in plaintext should be stored as  html
          frog_groups = format_and_process(MAIL_TEMPLATE_PLAINTEXT,
 -                                            to='groups@example.com', subject='Frogs Return', extra='',
 -                                            msg_id='<deadcafe.1337@smtp.agrolait.com>')
 +                                         to='groups@example.com', subject='Frogs Return', extra='',
 +                                         msg_id='<deadcafe.1337@smtp.agrolait.com>')
          # Test: one group created with one message
          self.assertEqual(len(frog_groups), 1, 'message_process: a new mail.group should have been created')
          frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
          msg = frog_group.message_ids[0]
          # Test: plain text content should be wrapped and stored as html
          self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', msg.body,
 -                            'message_process: plaintext incoming email incorrectly parsed')
 +                      'message_process: plaintext incoming email incorrectly parsed')
  
 +    @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
      def test_20_thread_parent_resolution(self):
          """ Testing parent/child relationships are correctly established when processing incoming mails """
          cr, uid = self.cr, self.uid
          # Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
          # 0. Direct alias match
          reply_msg1 = format(MAIL_TEMPLATE, to='Pretty Pigs <group+pigs@example.com>',
 -                                extra='In-Reply-To: %s' % msg1.message_id,
 -                                msg_id='<1198923581.41972151344608186760.JavaMail.2@agrolait.com>')
 +                            extra='In-Reply-To: %s' % msg1.message_id,
 +                            msg_id='<1198923581.41972151344608186760.JavaMail.2@agrolait.com>')
          self.mail_group.message_process(cr, uid, None, reply_msg1)
  
          # 1. In-Reply-To header
          reply_msg2 = format(MAIL_TEMPLATE, to='erroneous@example.com',
 -                                extra='In-Reply-To: %s' % msg1.message_id,
 -                                msg_id='<1198923581.41972151344608186760.JavaMail.3@agrolait.com>')
 +                            extra='In-Reply-To: %s' % msg1.message_id,
 +                            msg_id='<1198923581.41972151344608186760.JavaMail.3@agrolait.com>')
          self.mail_group.message_process(cr, uid, None, reply_msg2)
  
          # 2. References header
          reply_msg3 = format(MAIL_TEMPLATE, to='erroneous@example.com',
 -                                extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id,
 -                                msg_id='<1198923581.41972151344608186760.JavaMail.4@agrolait.com>')
 +                            extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id,
 +                            msg_id='<1198923581.41972151344608186760.JavaMail.4@agrolait.com>')
          self.mail_group.message_process(cr, uid, None, reply_msg3)
  
          # 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, but not to mail (not in msg1.child_ids)
          reply_msg4 = format(MAIL_TEMPLATE, to='erroneous@example.com',
 -                                extra='', subject='Re: [%s] 1' % self.group_pigs_id,
 -                                msg_id='<1198923581.41972151344608186760.JavaMail.5@agrolait.com>')
 +                            extra='', subject='Re: [%s] 1' % self.group_pigs_id,
 +                            msg_id='<1198923581.41972151344608186760.JavaMail.5@agrolait.com>')
          self.mail_group.message_process(cr, uid, 'mail.group', reply_msg4)
  
          group_pigs.refresh()
          """ Testing private discussion between partners. """
          cr, uid = self.cr, self.uid
  
 +        def format(template, to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1',
 +                                extra='', email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>',
 +                                msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>'):
 +            return template.format(to=to, subject=subject, extra=extra, email_from=email_from, msg_id=msg_id)
 +
          # Do: Raoul writes to Bert and Administrator, with a thread_model in context that should not be taken into account
          msg1_pids = [self.partner_admin_id, self.partner_bert_id]
 -        msg1_id = self.mail_thread.message_post(cr, self.user_raoul_id, False,
 -                        partner_ids=msg1_pids,
 -                        subtype='mail.mt_comment',
 -                        context={'thread_model': 'mail.group'})
 +        msg1_id = self.mail_thread.message_post(
 +            cr, self.user_raoul_id, False,
 +            partner_ids=msg1_pids,
 +            subtype='mail.mt_comment',
 +            context={'thread_model': 'mail.group'}
 +        )
  
          # Test: message recipients
          msg = self.mail_message.browse(cr, uid, msg1_id)
          test_pids = msg1_pids
          test_nids = msg1_pids
          self.assertEqual(set(msg_pids), set(test_pids),
 -                        'message_post: private discussion: incorrect recipients')
 +                         'message_post: private discussion: incorrect recipients')
          self.assertEqual(set(msg_nids), set(test_nids),
 -                        'message_post: private discussion: incorrect notified recipients')
 +                         'message_post: private discussion: incorrect notified recipients')
          self.assertEqual(msg.model, False,
 -                        'message_post: private discussion: context key "thread_model" not correctly ignored when having no res_id')
 +                         'message_post: private discussion: context key "thread_model" not correctly ignored when having no res_id')
 +        # Test: message-id
 +        self.assertIn('openerp-private', msg.message_id,
 +                      'message_post: private discussion: message-id should contain the private keyword')
  
          # Do: Bert replies through mailgateway (is a customer)
 -        msg2_id = self.mail_thread.message_post(cr, uid, False,
 -                        author_id=self.partner_bert_id,
 -                        parent_id=msg1_id, subtype='mail.mt_comment')
 +        reply_message = format(MAIL_TEMPLATE, to='not_important@mydomain.com',
 +                               email_from='bert@bert.fr',
 +                               extra='In-Reply-To: %s' % msg.message_id,
 +                               msg_id='<test30.JavaMail.0@agrolait.com>')
 +        self.mail_thread.message_process(cr, uid, None, reply_message)
 +
 +        # Test: last mail_message created
 +        msg2_id = self.mail_message.search(cr, uid, [], limit=1)[0]
  
          # Test: message recipients
          msg = self.mail_message.browse(cr, uid, msg2_id)
          msg_nids = [p.id for p in msg.notified_partner_ids]
          test_pids = [self.partner_admin_id, self.partner_raoul_id]
          test_nids = test_pids
 +        self.assertEqual(msg.author_id.id, self.partner_bert_id,
 +                         'message_post: private discussion: wrong author through mailgatewya based on email')
 +        self.assertEqual(set(msg_pids), set(test_pids),
 +                         'message_post: private discussion: incorrect recipients when replying')
 +        self.assertEqual(set(msg_nids), set(test_nids),
 +                         'message_post: private discussion: incorrect notified recipients when replying')
 +
 +        # Do: Bert replies through chatter (is a customer)
 +        msg3_id = self.mail_thread.message_post(
 +            cr, uid, False,
 +            author_id=self.partner_bert_id,
 +            parent_id=msg1_id, subtype='mail.mt_comment')
 +
 +        # Test: message recipients
 +        msg = self.mail_message.browse(cr, uid, msg3_id)
 +        msg_pids = [p.id for p in msg.partner_ids]
 +        msg_nids = [p.id for p in msg.notified_partner_ids]
 +        test_pids = [self.partner_admin_id, self.partner_raoul_id]
 +        test_nids = test_pids
          self.assertEqual(set(msg_pids), set(test_pids),
 -                        'message_post: private discussion: incorrect recipients when replying')
 +                         'message_post: private discussion: incorrect recipients when replying')
          self.assertEqual(set(msg_nids), set(test_nids),
 -                        'message_post: private discussion: incorrect notified recipients when replying')
 +                         'message_post: private discussion: incorrect notified recipients when replying')
  
          # Do: Administrator replies
 -        msg3_id = self.mail_thread.message_post(cr, uid, False,
 -                        parent_id=msg2_id, subtype='mail.mt_comment')
 +        msg3_id = self.mail_thread.message_post(cr, uid, False, parent_id=msg3_id, subtype='mail.mt_comment')
  
          # Test: message recipients
          msg = self.mail_message.browse(cr, uid, msg3_id)
          test_pids = [self.partner_bert_id, self.partner_raoul_id]
          test_nids = test_pids
          self.assertEqual(set(msg_pids), set(test_pids),
 -                        'message_post: private discussion: incorrect recipients when replying')
 +                         'message_post: private discussion: incorrect recipients when replying')
          self.assertEqual(set(msg_nids), set(test_nids),
 -                        'message_post: private discussion: incorrect notified recipients when replying')
 +                         'message_post: private discussion: incorrect notified recipients when replying')