From: Denis Ledoux Date: Mon, 17 Feb 2014 12:42:30 +0000 (+0100) Subject: [MERGE] Forward-port of latest 7.0 bugfixes, up to rev. 9846 revid:dle@openerp.com... X-Git-Tag: InsPy_master01~208^2~63^2~23 X-Git-Url: http://git.inspyration.org/?a=commitdiff_plain;h=0ef17beed026409a641ba09313a887614289d172;p=odoo%2Fodoo.git [MERGE] Forward-port of latest 7.0 bugfixes, up to rev. 9846 revid:dle@openerp.com-20140217124044-o8sgz1esfqeha01f 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 --- 0ef17beed026409a641ba09313a887614289d172 diff --cc addons/crm/crm_lead.py index 630c339,56aa3d4..a7450cf --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@@ -319,18 -343,19 +319,19 @@@ class crm_lead(format_address, osv.osv) if partner_id: partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) values = { - 'partner_name' : partner.name, - 'street' : partner.street, - 'street2' : partner.street2, - 'city' : partner.city, - 'state_id' : partner.state_id and partner.state_id.id or False, - 'country_id' : partner.country_id and partner.country_id.id or False, - 'email_from' : partner.email, - 'phone' : partner.phone, - 'mobile' : partner.mobile, - 'fax' : partner.fax, + 'partner_name': partner.name, + 'street': partner.street, + 'street2': partner.street2, + 'city': partner.city, + 'state_id': partner.state_id and partner.state_id.id or False, + 'country_id': partner.country_id and partner.country_id.id or False, + 'email_from': partner.email, + 'phone': partner.phone, + 'mobile': partner.mobile, + 'fax': partner.fax, + 'zip': partner.zip, } - return {'value' : values} + return {'value': values} def on_change_user(self, cr, uid, ids, user_id, context=None): """ When changing the user, also set a section_id or restrict section id diff --cc addons/mail/mail_thread.py index 71b6008,40a4fef..4c4a60b --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@@ -650,129 -470,27 +650,134 @@@ class mail_thread(osv.AbstractModel) def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None): """ Find partners related to some header fields of the message. - TDE TODO: merge me with other partner finding methods in 8.0 """ - partner_obj = self.pool.get('res.partner') - partner_ids = [] + :param string message: an email.message instance """ s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)]) - for email_address in tools.email_split(s): - related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context) - if not related_partners: - related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address)], limit=1, context=context) - partner_ids += related_partners - return partner_ids + return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context)) + + def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None): + """ Verify route validity. Check and rules: + 1 - if thread_id -> check that document effectively exists; otherwise + fallback on a message_new by resetting thread_id + 2 - check that message_update exists if thread_id is set; or at least + that message_new exist + [ - find author_id if udpate_author is set] + 3 - if there is an alias, check alias_contact: + 'followers' and thread_id: + check on target document that the author is in the followers + 'followers' and alias_parent_thread_id: + check on alias parent document that the author is in the + followers + 'partners': check that author_id id set + """ - def _message_find_user_id(self, cr, uid, message, context=None): - """ TDE TODO: check and maybe merge me with other user finding methods in 8.0 """ - from_local_part = tools.email_split(decode(message.get('From')))[0] - # FP Note: canonification required, the minimu: .lower() - user_ids = self.pool.get('res.users').search(cr, uid, ['|', - ('login', '=', from_local_part), - ('email', '=', from_local_part)], context=context) - return user_ids[0] if user_ids else uid + assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple' + assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record' - def message_route(self, cr, uid, message, model=None, thread_id=None, + message_id = message.get('Message-Id') + email_from = decode_header(message, 'From') + author_id = message_dict.get('author_id') + model, thread_id, alias = route[0], route[1], route[4] + model_pool = None + + def _create_bounce_email(): + mail_mail = self.pool.get('mail.mail') + mail_id = mail_mail.create(cr, uid, { + 'body_html': '

Hello,

' + '

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.

' + '
%s
' % (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. @@@ -803,12 -520,12 +808,15 @@@ :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') @@@ -890,52 -596,19 +898,57 @@@ 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, diff --cc addons/mail/tests/test_mail_gateway.py index b2a1c32,e7a2206..c756048 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@@ -465,18 -411,18 +465,18 @@@ class TestMailgateway(TestMail) # -------------------------------------------------- # Do: incoming email with model that does not accepts incoming emails must raise - self.assertRaises(AssertionError, + self.assertRaises(ValueError, - format_and_process, - MAIL_TEMPLATE, - to='noone@example.com', subject='spam', extra='', model='res.country', - msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>') + format_and_process, + MAIL_TEMPLATE, + to='noone@example.com', subject='spam', extra='', model='res.country', + msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>') # Do: incoming email without model and without alias must raise - self.assertRaises(AssertionError, + self.assertRaises(ValueError, - format_and_process, - MAIL_TEMPLATE, - to='noone@example.com', subject='spam', extra='', - msg_id='<1198923581.41972151344608186760.JavaMail.new5@agrolait.com>') + format_and_process, + MAIL_TEMPLATE, + to='noone@example.com', subject='spam', extra='', + msg_id='<1198923581.41972151344608186760.JavaMail.new5@agrolait.com>') # Do: incoming email with model that accepting incoming emails as fallback frog_groups = format_and_process(MAIL_TEMPLATE,