diff options
Diffstat (limited to 'addons/mail')
| -rw-r--r-- | addons/mail/mail_alias.py | 101 | ||||
| -rw-r--r-- | addons/mail/mail_alias_view.xml | 26 | ||||
| -rw-r--r-- | addons/mail/mail_group.py | 22 | ||||
| -rw-r--r-- | addons/mail/mail_group_view.xml | 24 | ||||
| -rw-r--r-- | addons/mail/mail_thread.py | 344 | ||||
| -rw-r--r-- | addons/mail/res_users.py | 29 | ||||
| -rw-r--r-- | addons/mail/res_users_view.xml | 34 | ||||
| -rw-r--r-- | addons/mail/static/src/css/mail.css | 10 | ||||
| -rw-r--r-- | addons/mail/static/src/js/mail.js | 11 | ||||
| -rw-r--r-- | addons/mail/tests/test_mail_base.py | 6 | ||||
| -rw-r--r-- | addons/mail/tests/test_mail_features.py | 6 | ||||
| -rw-r--r-- | addons/mail/tests/test_mail_gateway.py | 230 |
12 files changed, 509 insertions, 334 deletions
diff --git a/addons/mail/mail_alias.py b/addons/mail/mail_alias.py index 870695e074e..97d2842d15c 100644 --- a/addons/mail/mail_alias.py +++ b/addons/mail/mail_alias.py @@ -39,6 +39,7 @@ def remove_accents(input_str): nkfd_form = unicodedata.normalize('NFKD', input_str) return u''.join([c for c in nkfd_form if not unicodedata.combining(c)]) + class mail_alias(osv.Model): """A Mail Alias is a mapping of an email address with a given OpenERP Document model. It is used by OpenERP's mail gateway when processing incoming emails @@ -47,7 +48,7 @@ class mail_alias(osv.Model): of that alias. If the message is a reply it will be attached to the existing discussion on the corresponding record, otherwise a new record of the corresponding model will be created. - + This is meant to be used in combination with a catch-all email configuration on the company's mail server, so that as soon as a new mail.alias is created, it becomes immediately usable and OpenERP will accept email for it. @@ -63,9 +64,8 @@ class mail_alias(osv.Model): return dict.fromkeys(ids, domain or "") _columns = { - 'alias_name': fields.char('Alias', required=True, - help="The name of the email alias, e.g. 'jobs' " - "if you want to catch emails for <jobs@example.my.openerp.com>",), + 'alias_name': fields.char('Alias', + help="The name of the email alias, e.g. 'jobs' if you want to catch emails for <jobs@example.my.openerp.com>",), 'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade", help="The model (OpenERP Document Kind) to which this alias " "corresponds. Any incoming email that does not reply to an " @@ -87,13 +87,29 @@ class mail_alias(osv.Model): "messages will be attached, even if they did not reply to it. " "If set, this will disable the creation of new records completely."), 'alias_domain': fields.function(_get_alias_domain, string="Alias domain", type='char', size=None), + 'alias_parent_model_id': fields.many2one('ir.model', 'Parent Model', + help="Parent model holding the alias. The model holding the alias reference\n" + "is not necessarily the model given by alias_model_id\n" + "(example: project (parent_model) and task (model))"), + 'alias_parent_thread_id': fields.integer('Parent Record Thread ID', + help="ID of the parent record holding the alias (example: project holding the task creation alias)"), + 'alias_contact': fields.selection([ + ('everyone', 'Everyone'), + ('partners', 'Authenticated Partners'), + ('followers', 'Followers only'), + ], string='Alias Contact Security', required=True, + help="Policy to post a message on the document using the mailgateway.\n" + "- everyone: everyone can post\n" + "- partners: only authenticated partners\n" + "- followers: only followers of the related document\n"), } _defaults = { 'alias_defaults': '{}', - 'alias_user_id': lambda self,cr,uid,context: uid, + 'alias_user_id': lambda self, cr, uid, context: uid, # looks better when creating new aliases - even if the field is informative only - 'alias_domain': lambda self,cr,uid,context: self._get_alias_domain(cr, SUPERUSER_ID,[1],None,None)[1] + 'alias_domain': lambda self, cr, uid, context: self._get_alias_domain(cr, SUPERUSER_ID, [1], None, None)[1], + 'alias_contact': 'everyone', } _sql_constraints = [ @@ -139,13 +155,15 @@ class mail_alias(osv.Model): return new_name def migrate_to_alias(self, cr, child_model_name, child_table_name, child_model_auto_init_fct, - alias_id_column, alias_key, alias_prefix='', alias_force_key='', alias_defaults={}, context=None): + alias_model_name, alias_id_column, alias_key, alias_prefix='', alias_force_key='', alias_defaults={}, + alias_generate_name=False, context=None): """ Installation hook to create aliases for all users and avoid constraint errors. :param child_model_name: model name of the child class (i.e. res.users) :param child_table_name: table name of the child class (i.e. res_users) :param child_model_auto_init_fct: pointer to the _auto_init function (i.e. super(res_users,self)._auto_init(cr, context=context)) + :param alias_model_name: name of the aliased model :param alias_id_column: alias_id column (i.e. self._columns['alias_id']) :param alias_key: name of the column used for the unique name (i.e. 'login') :param alias_prefix: prefix for the unique name (i.e. 'jobs' + ...) @@ -153,6 +171,8 @@ class mail_alias(osv.Model): if empty string, not taken into account :param alias_defaults: dict, keys = mail.alias columns, values = child model column name used for default values (i.e. {'job_id': 'id'}) + :param alias_generate_name: automatically generate alias name using prefix / alias key; + default alias_name value is False because since 8.0 it is not required anymore """ if context is None: context = {} @@ -170,13 +190,17 @@ class mail_alias(osv.Model): no_alias_ids = child_class_model.search(cr, SUPERUSER_ID, [('alias_id', '=', False)], context={'active_test': False}) # Use read() not browse(), to avoid prefetching uninitialized inherited fields for obj_data in child_class_model.read(cr, SUPERUSER_ID, no_alias_ids, [alias_key]): - alias_vals = {'alias_name': '%s%s' % (alias_prefix, obj_data[alias_key])} + alias_vals = {'alias_name': False} + if alias_generate_name: + alias_vals['alias_name'] = '%s%s' % (alias_prefix, obj_data[alias_key]) if alias_force_key: alias_vals['alias_force_thread_id'] = obj_data[alias_force_key] alias_vals['alias_defaults'] = dict((k, obj_data[v]) for k, v in alias_defaults.iteritems()) - alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, alias_vals, model_name=context.get('alias_model_name', child_model_name)) + alias_vals['alias_parent_thread_id'] = obj_data['id'] + alias_create_ctx = dict(context, alias_model_name=alias_model_name, alias_parent_model_name=child_model_name) + alias_id = mail_alias.create(cr, SUPERUSER_ID, alias_vals, context=alias_create_ctx) child_class_model.write(cr, SUPERUSER_ID, obj_data['id'], {'alias_id': alias_id}) - _logger.info('Mail alias created for %s %s (uid %s)', child_model_name, obj_data[alias_key], obj_data['id']) + _logger.info('Mail alias created for %s %s (id %s)', child_model_name, obj_data[alias_key], obj_data['id']) # Finally attempt to reinstate the missing constraint try: @@ -189,22 +213,53 @@ class mail_alias(osv.Model): # set back the unique alias_id constraint alias_id_column.required = True - return res - def create_unique_alias(self, cr, uid, vals, model_name=None, context=None): - """Creates an email.alias record according to the values provided in ``vals``, - with 2 alterations: the ``alias_name`` value may be suffixed in order to - make it unique (and certain unsafe characters replaced), and - he ``alias_model_id`` value will set to the model ID of the ``model_name`` - value, if provided, + def create(self, cr, uid, vals, context=None): + """ Creates an email.alias record according to the values provided in ``vals``, + with 2 alterations: the ``alias_name`` value may be suffixed in order to + make it unique (and certain unsafe characters replaced), and + he ``alias_model_id`` value will set to the model ID of the ``model_name`` + context value, if provided. """ - # when an alias name appears to already be an email, we keep the local part only - alias_name = remove_accents(vals['alias_name']).lower().split('@')[0] - alias_name = re.sub(r'[^\w+.]+', '-', alias_name) - alias_name = self._find_unique(cr, uid, alias_name, context=context) - vals['alias_name'] = alias_name + if context is None: + context = {} + model_name = context.get('alias_model_name') + parent_model_name = context.get('alias_parent_model_name') + if vals.get('alias_name'): + # when an alias name appears to already be an email, we keep the local part only + alias_name = remove_accents(vals['alias_name']).lower().split('@')[0] + alias_name = re.sub(r'[^\w+.]+', '-', alias_name) + alias_name = self._find_unique(cr, uid, alias_name, context=context) + vals['alias_name'] = alias_name if model_name: model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0] vals['alias_model_id'] = model_id - return self.create(cr, uid, vals, context=context) + if parent_model_name: + model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', parent_model_name)], context=context)[0] + vals['alias_parent_model_id'] = model_id + return super(mail_alias, self).create(cr, uid, vals, context=context) + + def open_document(self, cr, uid, ids, context=None): + alias = self.browse(cr, uid, ids, context=context)[0] + if not alias.alias_model_id or not alias.alias_force_thread_id: + return False + return { + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': alias.alias_model_id.model, + 'res_id': alias.alias_force_thread_id, + 'type': 'ir.actions.act_window', + } + + def open_parent_document(self, cr, uid, ids, context=None): + alias = self.browse(cr, uid, ids, context=context)[0] + if not alias.alias_parent_model_id or not alias.alias_parent_thread_id: + return False + return { + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': alias.alias_parent_model_id.model, + 'res_id': alias.alias_parent_thread_id, + 'type': 'ir.actions.act_window', + } diff --git a/addons/mail/mail_alias_view.xml b/addons/mail/mail_alias_view.xml index e0d8e173ebc..248bf7e8f3a 100644 --- a/addons/mail/mail_alias_view.xml +++ b/addons/mail/mail_alias_view.xml @@ -9,13 +9,23 @@ <field name="arch" type="xml"> <form string="Alias" version="7.0"> <sheet> - <label for="alias_name" class="oe_edit_only"/> <h2><field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline"/></h2> + <div class="oe_right oe_button_box"> + <button name="open_document" string="Open Document" + type="object" class="oe_link" + attrs="{'invisible': ['|', ('alias_model_id', '=', False), ('alias_force_thread_id', '=', 0)]}"/> + <button name="open_parent_document" string="Open Parent Document" + type="object" class="oe_link" + attrs="{'invisible': ['|', ('alias_parent_model_id', '=', False), ('alias_parent_thread_id', '=', 0)]}"/> + </div> <group> <field name="alias_model_id"/> - <field name="alias_user_id"/> <field name="alias_force_thread_id"/> <field name="alias_defaults"/> + <field name="alias_contact"/> + <field name="alias_user_id"/> + <field name="alias_parent_model_id"/> + <field name="alias_parent_thread_id"/> </group> </sheet> </form> @@ -32,6 +42,7 @@ <field name="alias_model_id"/> <field name="alias_user_id"/> <field name="alias_defaults"/> + <field name="alias_contact"/> </tree> </field> </record> @@ -44,8 +55,13 @@ <search string="Search Alias"> <field name="alias_name"/> <field name="alias_model_id"/> + <field name="alias_force_thread_id"/> + <field name="alias_parent_model_id"/> + <field name="alias_parent_thread_id"/> + <separator/> + <filter string="Active" name="active" domain="[('alias_name', '!=', False)]"/> <group expand="0" string="Group By..."> - <filter string="User" name="User" icon="terp-personal" context="{'group_by':'alias_user_id'}"/> + <filter string="User" name="User" context="{'group_by':'alias_user_id'}"/> <filter string="Model" name="Model" context="{'group_by':'alias_model_id'}"/> </group> </search> @@ -55,6 +71,10 @@ <record id="action_view_mail_alias" model="ir.actions.act_window"> <field name="name">Aliases</field> <field name="res_model">mail.alias</field> + <field name="context">{ + 'search_default_active': True, + } + </field> </record> <menuitem id="mail_alias_menu" diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 9ca3738bcb2..9e363065481 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -93,17 +93,16 @@ class mail_group(osv.Model): 'public': 'groups', 'group_public_id': _get_default_employee_group, 'image': _get_default_image, - 'alias_domain': False, # always hide alias during creation } def _generate_header_description(self, cr, uid, group, context=None): header = '' if group.description: header = '%s' % group.description - if group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain: + if group.alias_id and group.alias_name and group.alias_domain: if header: header = '%s<br/>' % header - return '%sGroup email gateway: %s@%s' % (header, group.alias_id.alias_name, group.alias_id.alias_domain) + return '%sGroup email gateway: %s@%s' % (header, group.alias_name, group.alias_domain) return header def _subscribe_users(self, cr, uid, ids, context=None): @@ -114,15 +113,8 @@ class mail_group(osv.Model): self.message_subscribe(cr, uid, ids, partner_ids, context=context) def create(self, cr, uid, vals, context=None): - mail_alias = self.pool.get('mail.alias') - if not vals.get('alias_id'): - vals.pop('alias_name', None) # prevent errors during copy() - alias_id = mail_alias.create_unique_alias(cr, uid, - # Using '+' allows using subaddressing for those who don't - # have a catchall domain setup. - {'alias_name': "group+" + vals['name']}, - model_name=self._name, context=context) - vals['alias_id'] = alias_id + if context is None: + context = {} # get parent menu menu_parent = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'mail_group_root') @@ -134,8 +126,10 @@ class mail_group(osv.Model): vals['menu_id'] = menu_id # Create group and alias - mail_group_id = super(mail_group, self).create(cr, uid, vals, context=context) - mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context) + create_context = dict(context, alias_model_name=self._name, alias_parent_model_name=self._name) + mail_group_id = super(mail_group, self).create(cr, uid, vals, context=create_context) + group = self.browse(cr, uid, mail_group_id, context=context) + self.pool.get('mail.alias').write(cr, uid, [group.alias_id.id], {"alias_force_thread_id": mail_group_id, 'alias_parent_thread_id': mail_group_id}, context) group = self.browse(cr, uid, mail_group_id, context=context) # Create client action for this group and link the menu to it diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml index 04212b2f58a..8bf61dbdb1e 100644 --- a/addons/mail/mail_group_view.xml +++ b/addons/mail/mail_group_view.xml @@ -44,7 +44,7 @@ <div class="oe_group_details"> <h4><a type="open"><field name="name"/></a></h4> <div class="oe_kanban_alias" t-if="record.alias_id.value"> - <span class="oe_e">%%</span><small><field name="alias_id"/></small> + <span class="oe_e oe_e_alias">%%</span><small><field name="alias_id"/></small> </div> <div class="oe_grey"> <field name="description"/> @@ -78,17 +78,19 @@ <label for="name" string="Group Name"/> </div> <h1><field name="name" readonly="0"/></h1> - <div name="group_alias" + <group colspan="2" name="group_alias" attrs="{'invisible': [('alias_domain', '=', False)]}"> - <label for="alias_id" string="Email Alias"/> - <field name="alias_id" class="oe_inline oe_read_only" required="0" nolabel="1"/> - <span name="edit_alias" class="oe_edit_only"> - <field name="alias_name" class="oe_inline" - attrs="{'required': [('alias_id', '!=', False)]}"/> - @ - <field name="alias_domain" class="oe_inline" readonly="1"/> - </span> - </div> + <label for="alias_id" string="%%" class="oe_e oe_e_alias" style="min-width: 20px;"/> + <div name="alias_def"> + <field name="alias_id" class="oe_read_only oe_inline" + string="Email Alias" required="0"/> + <div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" > + <field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/> + </div> + </div> + <label for="alias_contact" string="V" class="oe_e oe_e_alias" style="min-width: 20px;"/> + <field name="alias_contact" class="oe_inline" nolabel="1"/> + </group> </div> <field name="description" placeholder="Topics discussed in this group..."/> <div class="oe_clear"/> diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index c6b0dd9f5fa..403cfab7d61 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -25,7 +25,6 @@ import dateutil import email import logging import pytz -import re import time import xmlrpclib from email.message import Message @@ -102,21 +101,22 @@ class mail_thread(osv.AbstractModel): 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_model_id and \ + 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_force_thread_id', '=', 0)], context=context, order='id ASC') + 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 a new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a> + Click here to add new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a> </p> %(static_help)s""" ) % { @@ -126,7 +126,7 @@ class mail_thread(osv.AbstractModel): } 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 a new %(document)s</p>%(static_help)s") % { + 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 '', } @@ -257,7 +257,6 @@ class mail_thread(osv.AbstractModel): def _search_is_follower(self, cr, uid, obj, name, args, context): """Search function for message_is_follower""" - fol_obj = self.pool.get('mail.followers') res = [] for field, operator, value in args: assert field == name @@ -564,6 +563,8 @@ class mail_thread(osv.AbstractModel): #------------------------------------------------------ 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']) @@ -587,27 +588,123 @@ class mail_thread(osv.AbstractModel): def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None): """ Find partners related to some header fields of the message. - TDE TODO: merge me with other partner finding methods in 8.0 """ - partner_obj = self.pool.get('res.partner') - partner_ids = [] + :param string message: an email.message instance """ s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)]) - for email_address in tools.email_split(s): - related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context) - if not related_partners: - related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address)], limit=1, context=context) - partner_ids += related_partners - return partner_ids + return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context)) + + def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None): + """ Verify route validity. Check and rules: + 1 - if thread_id -> check that document effectively exists; otherwise + fallback on a message_new by resetting thread_id + 2 - check that message_update exists if thread_id is set; or at least + that message_new exist + [ - find author_id if udpate_author is set] + 3 - if there is an alias, check alias_contact: + 'followers' and thread_id: + check on target document that the author is in the followers + 'followers' and alias_parent_thread_id: + check on alias parent document that the author is in the + followers + 'partners': check that author_id id set + """ - def _message_find_user_id(self, cr, uid, message, context=None): - """ TDE TODO: check and maybe merge me with other user finding methods in 8.0 """ - from_local_part = tools.email_split(decode(message.get('From')))[0] - # FP Note: canonification required, the minimu: .lower() - user_ids = self.pool.get('res.users').search(cr, uid, ['|', - ('login', '=', from_local_part), - ('email', '=', from_local_part)], context=context) - return user_ids[0] if user_ids else uid + assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple' + assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record' - def message_route(self, cr, uid, message, model=None, thread_id=None, + message_id = message.get('Message-Id') + email_from = decode_header(message, 'From') + author_id = message_dict.get('author_id') + model, thread_id, alias = route[0], route[1], route[4] + model_pool = None + + def _create_bounce_email(): + mail_mail = self.pool.get('mail.mail') + mail_id = mail_mail.create(cr, uid, { + 'body_html': '<div><p>Hello,</p>' + '<p>The following email sent to %s cannot be accepted because this is ' + 'a private email address. Only allowed people can contact us at this address.</p></div>' + '<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')), + 'subject': 'Re: %s' % message.get('subject'), + 'email_to': message.get('from'), + 'auto_delete': True, + }, context=context) + mail_mail.send(cr, uid, [mail_id], context=context) + + def _warn(message): + _logger.warning('Routing mail with Message-Id %s: route %s: %s', + message_id, route, message) + + # Wrong model + if model and not model in self.pool: + if assert_model: + assert model in self.pool, 'Routing: unknown target model %s' % model + _warn('unknown target model %s' % model) + return () + elif model: + model_pool = self.pool[model] + + # Private message: should not contain any thread_id + if not model and thread_id: + if assert_model: + assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message).' + _warn('posting a message without model should be with a null res_id (private message), resetting thread_id') + thread_id = 0 + + # 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 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 not hasattr(model_pool, 'message_new'): + if assert_model: + assert hasattr(model_pool, 'message_new'), '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. @@ -627,6 +724,7 @@ class mail_thread(osv.AbstractModel): 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) @@ -637,9 +735,12 @@ class mail_thread(osv.AbstractModel): :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] """ assert isinstance(message, Message), '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') @@ -649,18 +750,20 @@ class mail_thread(osv.AbstractModel): # 1. Verify if this is a reply to an existing thread thread_references = references or in_reply_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 = 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) - return [(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 [] - # Verify whether this is a reply to a private message + # 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), @@ -670,9 +773,12 @@ class mail_thread(osv.AbstractModel): message = self.pool.get('mail.message').browse(cr, uid, 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)] + route = self.message_route_verify(cr, uid, message, message_dict, + (message.model, 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 = \ @@ -697,14 +803,16 @@ class mail_thread(osv.AbstractModel): # 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')) @@ -714,16 +822,18 @@ class mail_thread(osv.AbstractModel): thread_id = int(thread_id) except: thread_id = False - assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ + _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) - 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_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, @@ -777,25 +887,21 @@ class mail_thread(osv.AbstractModel): msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context) if strip_attachments: msg.pop('attachments', None) + # postpone setting msg.partner_ids after message_post, to avoid double notifications + partner_ids = msg.pop('partner_ids', []) 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', []) - + routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context) thread_id = False - for model, thread_id, custom_values, user_id in routes: + for model, thread_id, custom_values, user_id, alias in routes: if self._name == 'mail.thread': context.update({'thread_model': model}) if model: @@ -806,11 +912,10 @@ class mail_thread(osv.AbstractModel): # 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) + 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], 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: assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." @@ -947,7 +1052,6 @@ class mail_thread(osv.AbstractModel): """ msg_dict = { 'type': 'email', - 'author_id': False, } if not isinstance(message, Message): if isinstance(message, unicode): @@ -970,12 +1074,7 @@ class mail_thread(osv.AbstractModel): 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] @@ -1029,7 +1128,7 @@ class mail_thread(osv.AbstractModel): 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)[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.get('partner_id')], context=context)[0] if email and email in [val[1] for val in result[obj.id]]: # already existing email -> skip @@ -1057,53 +1156,76 @@ class mail_thread(osv.AbstractModel): 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: @@ -1156,18 +1278,7 @@ class mail_thread(osv.AbstractModel): del context['thread_model'] 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: 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] + #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) @@ -1279,21 +1390,6 @@ class mail_thread(osv.AbstractModel): return msg_id #------------------------------------------------------ - # Compatibility methods: do not use - # TDE TODO: remove me in 8.0 - #------------------------------------------------------ - - def message_create_partners_from_emails(self, cr, uid, emails, context=None): - return {'partner_ids': [], 'new_partner_ids': []} - - def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False, - attachment_ids=None, content_subtype='plaintext', - context=None, **kwargs): - return self.message_post(cr, uid, thread_id, body=body, parent_id=parent_id, - attachment_ids=attachment_ids, content_subtype=content_subtype, - context=context, **kwargs) - - #------------------------------------------------------ # Followers API #------------------------------------------------------ diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index b9883489ca6..7bc39c438d1 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -23,6 +23,7 @@ from openerp.osv import fields, osv from openerp import SUPERUSER_ID from openerp.tools.translate import _ + class res_users(osv.Model): """ Update of res.users class - add a preference about sending emails about notifications @@ -42,7 +43,6 @@ class res_users(osv.Model): } _defaults = { - 'alias_domain': False, # always hide alias during creation 'display_groups_suggestions': True, } @@ -63,25 +63,20 @@ class res_users(osv.Model): def _auto_init(self, cr, context=None): """ Installation hook: aliases, partner following themselves """ # create aliases for all users and avoid constraint errors - res = self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(res_users, self)._auto_init, - self._columns['alias_id'], 'login', alias_force_key='id', context=context) - return res + return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(res_users, self)._auto_init, + self._name, self._columns['alias_id'], 'login', alias_force_key='id', context=context) def create(self, cr, uid, data, context=None): - # create default alias same as the login if not data.get('login', False): raise osv.except_osv(_('Invalid Action!'), _('You may not create a user. To create new users, you should use the "Settings > Users" menu.')) + if context is None: + context = {} - mail_alias = self.pool.get('mail.alias') - alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context) - data['alias_id'] = alias_id - data.pop('alias_name', None) # prevent errors during copy() - - # create user - user_id = super(res_users, self).create(cr, uid, data, context=context) + create_context = dict(context, alias_model_name=self._name, alias_parent_model_name=self._name) + user_id = super(res_users, self).create(cr, uid, data, context=create_context) user = self.browse(cr, uid, user_id, context=context) - # alias - mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context) + self.pool.get('mail.alias').write(cr, SUPERUSER_ID, [user.alias_id.id], {"alias_force_thread_id": user_id, "alias_parent_thread_id": user_id}, context) + # create a welcome message self._create_welcome_message(cr, uid, user, context=context) return user_id @@ -95,12 +90,6 @@ class res_users(osv.Model): return self.pool.get('res.partner').message_post(cr, SUPERUSER_ID, [user.partner_id.id], body=body, context=context) - def write(self, cr, uid, ids, vals, context=None): - # User alias is sync'ed with login - if vals.get('login'): - vals['alias_name'] = vals['login'] - return super(res_users, self).write(cr, uid, ids, vals, context=context) - def unlink(self, cr, uid, ids, context=None): # Cascade-delete mail aliases as well, as they should not exist without the user. alias_pool = self.pool.get('mail.alias') diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml index babe922d975..2bf54b4bafd 100644 --- a/addons/mail/res_users_view.xml +++ b/addons/mail/res_users_view.xml @@ -22,21 +22,25 @@ <field name="model">res.users</field> <field name="inherit_id" ref="base.view_users_form"/> <field name="arch" type="xml"> - <data> - <field name="signature" position="before"> - <field name="notification_email_send"/> - </field> - <field name="signature" position="before"> - <field name="alias_domain" invisible="1"/> - <field name="alias_id" readonly="1" required="0" attrs="{'invisible': [('alias_domain', '=', False)]}"/> - </field> - <group string="Email preferences" position="after"> - <group name="misc" string="Miscellaneous" - groups="base.group_no_one"> - <field name="display_groups_suggestions"/> - </group> - </group> - </data> + <data> + <field name="signature" position="before"> + <field name="notification_email_send"/> + </field> + <field name="signature" position="before"> + <label for="alias_id" string="Messaging Alias" class="oe_read_only" + attrs="{'invisible': [('alias_domain', '=', False)]}"/> + <field name="alias_id" class="oe_read_only" required="0" nolabel="1" + attrs="{'invisible': [('alias_domain', '=', False)]}"/> + <label for="alias_name" string="Messaging Alias" class="oe_edit_only" + attrs="{'invisible': [('alias_domain', '=', False)]}"/> + <div class="oe_edit_only" attrs="{'invisible': [('alias_domain', '=', False)]}"> + <field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/> + </div> + <field name="alias_contact" string="Alias Accepts Emails From" + attrs="{'invisible': [('alias_domain', '=', False)]}"/> + <field name="display_groups_suggestions" groups="base.group_no_one"/> + </field> + </data> </field> </record> diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index c969d3f3b16..211133afaf8 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -26,6 +26,16 @@ 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; diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 83ac92404f1..21b0f5a9a87 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -632,10 +632,7 @@ openerp.mail = function (session) { // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id var find_done = $.Deferred(); if (names_to_find.length > 0) { - var values = { - 'res_id': this.context.default_res_id, - } - find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [names_to_find], values); + find_done = self.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [this.context.default_res_id, names_to_find]); } else { find_done.resolve([]); @@ -681,11 +678,7 @@ openerp.mail = function (session) { var new_names_to_find = _.difference(names_to_find, names_to_remove); find_done = $.Deferred(); if (new_names_to_find.length > 0) { - var values = { - 'link_mail': true, - 'res_id': self.context.default_res_id, - } - find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [new_names_to_find], values); + find_done = self.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [self.context.default_res_id, new_names_to_find, true]); } else { find_done.resolve([]); diff --git a/addons/mail/tests/test_mail_base.py b/addons/mail/tests/test_mail_base.py index 7ca8b93135c..b0f9f72b6f7 100644 --- a/addons/mail/tests/test_mail_base.py +++ b/addons/mail/tests/test_mail_base.py @@ -71,9 +71,9 @@ class TestMailBase(common.TransactionCase): # Test users to use through the various tests self.res_users.write(cr, uid, uid, {'name': 'Administrator'}) self.user_raoul_id = self.res_users.create(cr, uid, - {'name': 'Raoul Grosbedon', 'signature': 'SignRaoul', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'groups_id': [(6, 0, [self.group_employee_id])]}) + {'name': 'Raoul Grosbedon', 'signature': 'SignRaoul', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'alias_name': 'raoul', 'groups_id': [(6, 0, [self.group_employee_id])]}) self.user_bert_id = self.res_users.create(cr, uid, - {'name': 'Bert Tartignole', 'signature': 'SignBert', 'email': 'bert@bert.fr', 'login': 'bert', 'groups_id': [(6, 0, [])]}) + {'name': 'Bert Tartignole', 'signature': 'SignBert', 'email': 'bert@bert.fr', 'login': 'bert', 'alias_name': 'bert', 'groups_id': [(6, 0, [])]}) self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id) self.user_bert = self.res_users.browse(cr, uid, self.user_bert_id) self.user_admin = self.res_users.browse(cr, uid, uid) @@ -83,7 +83,7 @@ class TestMailBase(common.TransactionCase): # Test 'pigs' group to use through the various tests self.group_pigs_id = self.mail_group.create(cr, uid, - {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}, + {'name': 'Pigs', 'description': 'Fans of Pigs, unite !', 'alias_name': 'group+pigs'}, {'mail_create_nolog': True}) self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index ca9c156c8cb..91ede8b8449 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -32,17 +32,17 @@ class test_mail(TestMailBase): """ Test basic mail.alias setup works, before trying to use them for routing """ cr, uid = self.cr, self.uid self.user_valentin_id = self.res_users.create(cr, uid, - {'name': 'Valentin Cognito', 'email': 'valentin.cognito@gmail.com', 'login': 'valentin.cognito'}) + {'name': 'Valentin Cognito', 'email': 'valentin.cognito@gmail.com', 'login': 'valentin.cognito', 'alias_name': 'valentin.cognito'}) self.user_valentin = self.res_users.browse(cr, uid, self.user_valentin_id) self.assertEquals(self.user_valentin.alias_name, self.user_valentin.login, "Login should be used as alias") self.user_pagan_id = self.res_users.create(cr, uid, - {'name': 'Pagan Le Marchant', 'email': 'plmarchant@gmail.com', 'login': 'plmarchant@gmail.com'}) + {'name': 'Pagan Le Marchant', 'email': 'plmarchant@gmail.com', 'login': 'plmarchant@gmail.com', 'alias_name': 'plmarchant@gmail.com'}) self.user_pagan = self.res_users.browse(cr, uid, self.user_pagan_id) self.assertEquals(self.user_pagan.alias_name, 'plmarchant', "If login is an email, the alias should keep only the local part") self.user_barty_id = self.res_users.create(cr, uid, - {'name': 'Bartholomew Ironside', 'email': 'barty@gmail.com', 'login': 'b4r+_#_R3wl$$'}) + {'name': 'Bartholomew Ironside', 'email': 'barty@gmail.com', 'login': 'b4r+_#_R3wl$$', 'alias_name': 'b4r+_#_R3wl$$'}) self.user_barty = self.res_users.browse(cr, uid, self.user_barty_id) self.assertEquals(self.user_barty.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens') diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index 9e05bb049c1..2e0ba336dc4 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -99,20 +99,20 @@ class TestMailgateway(TestMailBase): # -------------------------------------------------- # 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] + partner_info = self.mail_thread.message_partner_info_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') + 'mail_thread: message_partner_info_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') + 'mail_thread: message_partner_info_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] + partner_info = self.mail_thread.message_partner_info_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') + 'mail_thread: message_partner_info_from_emails wrong partner found') # -------------------------------------------------- # CASE1: with object @@ -120,9 +120,9 @@ class TestMailgateway(TestMailBase): # 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] + partner_info = self.mail_group.message_partner_info_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') + 'mail_thread: message_partner_info_from_emails wrong partner found') def test_05_mail_message_mail_mail(self): """ Tests designed for testing email values based on mail.message, aliases, ... """ @@ -189,6 +189,7 @@ class TestMailgateway(TestMailBase): # Data: set catchall domain self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', alias_domain) + self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')])) # Update message self.mail_message.write(cr, user_raoul_id, [msg_id], {'email_from': False, 'reply_to': False}) @@ -220,87 +221,6 @@ class TestMailgateway(TestMailBase): self.assertEqual(mail.reply_to, msg.email_from, 'mail_mail: incorrect reply_to: should be message email_from') - def test_05_mail_message_mail_mail(self): - """ Tests designed for testing email values based on mail.message, aliases, ... """ - cr, uid = self.cr, self.uid - - # Data: clean catchall domain - param_ids = self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.domain')]) - self.registry('ir.config_parameter').unlink(cr, uid, param_ids) - - # Do: create a mail_message with a reply_to, without message-id - msg_id = self.mail_message.create(cr, uid, {'subject': 'Subject', 'body': 'Body', 'reply_to': 'custom@example.com'}) - msg = self.mail_message.browse(cr, uid, msg_id) - # Test: message content - self.assertIn('reply_to', msg.message_id, - 'mail_message: message_id should be specific to a mail_message with a given reply_to') - self.assertEqual('custom@example.com', msg.reply_to, - 'mail_message: incorrect reply_to') - # Do: create a mail_mail with the previous mail_message and specified reply_to - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'reply_to': 'other@example.com', 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, 'other@example.com', - 'mail_mail: reply_to should be equal to the one coming from creation values') - # Do: create a mail_mail with the previous mail_message - self.mail_message.write(cr, uid, [msg_id], {'reply_to': 'custom@example.com'}) - msg.refresh() - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, msg.reply_to, - 'mail_mail: reply_to should be equal to the one coming from the mail_message') - - # Do: create a mail_message without a reply_to - msg_id = self.mail_message.create(cr, uid, {'subject': 'Subject', 'body': 'Body', 'model': 'mail.group', 'res_id': self.group_pigs_id, 'email_from': False}) - msg = self.mail_message.browse(cr, uid, msg_id) - # Test: message content - self.assertIn('mail.group', msg.message_id, - 'mail_message: message_id should contain model') - self.assertIn('%s' % self.group_pigs_id, msg.message_id, - 'mail_message: message_id should contain res_id') - self.assertFalse(msg.reply_to, - 'mail_message: should not generate a reply_to address when not specified') - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertFalse(mail.reply_to, - 'mail_mail: reply_to should not have been guessed') - # Update message - self.mail_message.write(cr, uid, [msg_id], {'email_from': 'someone@example.com'}) - msg.refresh() - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(email_split(mail.reply_to), email_split(msg.email_from), - 'mail_mail: reply_to should be equal to mail_message.email_from when having no document or default alias') - - # Data: set catchall domain - self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', 'schlouby.fr') - self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')])) - - # Update message - self.mail_message.write(cr, uid, [msg_id], {'email_from': False, 'reply_to': False}) - msg.refresh() - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, '"Followers of Pigs" <group+pigs@schlouby.fr>', - 'mail_mail: reply_to should equal the mail.group alias') - - # Update message - self.mail_message.write(cr, uid, [msg_id], {'res_id': False, 'email_from': 'someone@schlouby.fr', 'reply_to': False}) - msg.refresh() - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, msg.email_from, - 'mail_mail: reply_to should equal the mail_message email_from') - # Data: set catchall alias self.registry('ir.config_parameter').set_param(self.cr, self.uid, 'mail.catchall.alias', 'gateway') @@ -310,7 +230,7 @@ class TestMailgateway(TestMailBase): # Do: create a mail_mail based on the previous mail_message mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content + # Test: mail_mail Content-Type self.assertEqual(mail.reply_to, 'gateway@schlouby.fr', 'mail_mail: reply_to should equal the catchall email alias') @@ -351,7 +271,10 @@ class TestMailgateway(TestMailBase): 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 @@ -392,12 +315,42 @@ class TestMailgateway(TestMailBase): # 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') @@ -409,24 +362,37 @@ class TestMailgateway(TestMailBase): self.assertEqual(len(frog_group.message_ids), 1, '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') 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 + # 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)') # 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') # 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]) @@ -438,15 +404,63 @@ class TestMailgateway(TestMailBase): self.assertEqual(frog_follower_ids, set([p1id]), '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') # -------------------------------------------------- - # Test2: discussion update + # Test2: update-like alias + # -------------------------------------------------- + + # 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') + 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: 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='other@gmail.com', + 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) @@ -458,14 +472,14 @@ class TestMailgateway(TestMailBase): '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') + 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') # Do: due to some issue, same email goes back into the mailgateway - frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other@gmail.com', + 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 @@ -476,20 +490,18 @@ class TestMailgateway(TestMailBase): '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') # -------------------------------------------------- - # 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'}) - # extra_user_id = self.res_users.create(cr, uid, {'name': 'B-Raoul', 'email': self.user_raoul.email}) - # extra_user_pid = self.res_users.browse(cr, uid, extra_user_id).partner_id.id # 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>', @@ -534,7 +546,7 @@ class TestMailgateway(TestMailBase): 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 @@ -568,7 +580,7 @@ class TestMailgateway(TestMailBase): 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.assertEqual(msg.body, '<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', + 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') @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm') |
