From dbaeee703fc16027bf17552acc6bb3c409d21958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 22 Mar 2013 16:57:34 +0100 Subject: [IMP] mail: alias_contact on alias. However, not sure this will be usefull, to discuss. bzr revid: tde@openerp.com-20130322155734-9jfc7w8nw8oiytd4 --- addons/mail/mail_thread.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 7ca9fcb7b53..12e280594d4 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -553,9 +553,26 @@ class mail_thread(osv.AbstractModel): mail_alias = self.pool.get('mail.alias') alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)]) if alias_ids: + # TDE note: at this stage, message parsing is not done yet - parse the from of the message + author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context) routes = [] for alias in mail_alias.browse(cr, uid, alias_ids, context=context): user_id = alias.alias_user_id.id + + if alias.alias_contact == 'partners' and not author_ids: + # TDE TODO: bounce + _logger.info('Routing mail with Message-Id %s: alias %s does not accept unknown emails, skipping', message_id, alias.alias_name) + mail_id = self.pool.get('mail.mail').create(cr, uid, { + 'body_html': '

Hello,

' + '

The following email sent to %s cannot be accepted because this address' + 'is private. Only known contacts are allowed to contact this address.

' + '
%s
' % (message.get('to'), message.get('body')), + 'subject': message.get('subject'), + 'email_to': message.get('from'), + 'auto_delete': True, + }, context=context) + self.pool.get('mail.mail').send(cr, uid, [mail_id], context=context) + continue if not user_id: # TDE note: this could cause crashes, because no clue that the user # that send the email has the right to create or modify a new document -- cgit v1.2.3 From 7f6a47559094c57d2e858e1749b6c37237f96582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 16 Apr 2013 15:28:13 +0200 Subject: [IMP] [REF] mail_thread: refactored a bit message_parse, message_route and various find_partner methods. Now handles private aliases (alias_contact as partners or followers). message_route now works in 2 steps : 1/ finding all possible routes, as before 2/ checks and filtrates those routes, by finding the most relevant message author and using it to check alias contact settings. Also refactored the various partner finding method in one method that checks in res.partner, for: partners, partners that are users, partners that follow a given document. bzr revid: tde@openerp.com-20130416132813-zcm1r5kx0dwrfeic --- addons/mail/mail_thread.py | 380 ++++++++++++++++++++++++--------------------- 1 file changed, 205 insertions(+), 175 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 37336aeb6e7..e9b43d41f72 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -429,6 +429,9 @@ class mail_thread(osv.AbstractModel): return [('message_unread', '=', True)] return [] + # def _get_model(self, cr, uid, context=None): + # return if self._name == 'mail.thread' + def _garbage_collect_attachments(self, cr, uid, context=None): """ Garbage collect lost mail attachments. Those are attachments - linked to res_model 'mail.compose.message', the composer wizard @@ -449,11 +452,85 @@ class mail_thread(osv.AbstractModel): ir_attachment_obj.unlink(cr, uid, attach_ids, context=context) return True + def _find_partner_from_emails(self, cr, uid, id, emails, context=None, + check_followers=True, only_users=False, take_all=False, full_info=False): + """ Utility method to find partners + + :param boolean check_followers: TODO + :param boolean only_users: TODO + :param boolean take_all: TODO + """ + partner_obj = self.pool['res.partner'] + partner_ids = [] + obj = None + if id and self._name != 'mail.thread' and check_followers: + 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_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 """ + mail_message_obj = self.pool.get('mail.message') + partner_ids = self._find_partner_from_emails(cr, uid, id, emails, context=context) + result = list() + 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_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 + #------------------------------------------------------ # 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']) @@ -477,27 +554,11 @@ 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 - - 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 + return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context)) - def message_route(self, cr, uid, message, model=None, thread_id=None, + 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. @@ -516,6 +577,7 @@ class mail_thread(osv.AbstractModel): provided. 4. If all the above fails, raise an exception. + :param dict message_dict: dictionary holding message variables :param string message: an email.message instance :param string model: the fallback model to use if the message does not match any of the currently configured mail aliases @@ -530,28 +592,53 @@ class mail_thread(osv.AbstractModel): :return: list of [model, thread_id, custom_values, user_id] """ assert isinstance(message, Message), 'message must be an email.message.Message at this point' + found_routes = [] + DEFAULT_CONTACT_SETTINGS = 'everyone' + fallback_model = model + avoid_assert = False + + # 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') references = decode_header(message, 'References') in_reply_to = decode_header(message, 'In-Reply-To') - - # 1. Verify if this is a reply to an existing thread thread_references = references or in_reply_to + # 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 = ','.join([decode_header(message, 'Delivered-To'), + decode_header(message, 'To'), + decode_header(message, 'Cc'), + decode_header(message, 'Resent-To'), + decode_header(message, 'Resent-Cc')]) + + 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 address' + 'is private. Only known contacts are allowed to contact this address.

' + '
%s
' % (message.get('to'), message.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) + + # 1. Reply to an existing thread 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)] + found_routes = [(model, thread_id, DEFAULT_CONTACT_SETTINGS, custom_values, uid)] - # Verify whether this is a reply to a private message - if in_reply_to: + # 2. Reply to a private message + if not found_routes and in_reply_to: message_ids = self.pool.get('mail.message').search(cr, uid, [ ('message_id', '=', in_reply_to), '!', ('message_id', 'ilike', 'reply_to') @@ -560,77 +647,95 @@ 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)] + found_routes = [(message.model, message.res_id, DEFAULT_CONTACT_SETTINGS, custom_values, uid)] - # 2. 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 = \ - ','.join([decode_header(message, 'Delivered-To'), - decode_header(message, 'To'), - decode_header(message, 'Cc'), - decode_header(message, 'Resent-To'), - decode_header(message, 'Resent-Cc')]) + # 3. Look for a matching mail.alias entry local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)] - if local_parts: + if not found_routes and local_parts: mail_alias = self.pool.get('mail.alias') alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)]) - if alias_ids: - # TDE note: at this stage, message parsing is not done yet - parse the from of the message - author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context) - routes = [] - for alias in mail_alias.browse(cr, uid, alias_ids, context=context): - user_id = alias.alias_user_id.id - - if alias.alias_contact == 'partners' and not author_ids: - # TDE TODO: bounce - _logger.info('Routing mail with Message-Id %s: alias %s does not accept unknown emails, skipping', message_id, alias.alias_name) - mail_id = self.pool.get('mail.mail').create(cr, uid, { - 'body_html': '

Hello,

' - '

The following email sent to %s cannot be accepted because this address' - 'is private. Only known contacts are allowed to contact this address.

' - '
%s
' % (message.get('to'), message.get('body')), - 'subject': message.get('subject'), - 'email_to': message.get('from'), - 'auto_delete': True, - }, context=context) - self.pool.get('mail.mail').send(cr, uid, [mail_id], context=context) - continue - if not user_id: - # TDE note: this could cause crashes, because no clue that the user - # that send the email has the right to create or modify a new document - # Fallback on user_id = uid - # Note: recognized partners will be added as followers anyway - # user_id = self._message_find_user_id(cr, uid, message, context=context) - user_id = uid - _logger.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)) + for alias in mail_alias.browse(cr, uid, alias_ids, context=context): + user_id = alias.alias_user_id.id + if not user_id: + # TDE note: this could cause crashes, because no clue that the user + # that send the email has the right to create or modify a new document + # Fallback on user_id = uid + # Note: recognized partners will be added as followers anyway + # user_id = self._message_find_user_id(cr, uid, message, context=context) + user_id = uid + _logger.info('No matching user_id for the alias %s', alias.alias_name) + found_routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \ + alias.alias_contact, eval(alias.alias_defaults), user_id)) + if found_routes: _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r', - email_from, email_to, message_id, routes) - return routes - - # 3. Fallback to the provided parameters, if they work - model_pool = self.pool.get(model) - if not thread_id: - # Legacy: fallback to matching [ID] in the Subject - match = tools.res_re.search(decode_header(message, 'Subject')) - thread_id = match and match.group(1) - # Convert into int (bug spotted in 7.0 because of str) - try: - thread_id = int(thread_id) - except: - thread_id = False - assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ - "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)] + email_from, email_to, message_id, found_routes) + + # 4. Fallback to the provided parameters, if they work + if not found_routes: + if not thread_id: + # Legacy: fallback to matching [ID] in the Subject + match = tools.res_re.search(decode_header(message, 'Subject')) + thread_id = match and match.group(1) + # Convert into int (bug spotted in 7.0 because of str) + try: + thread_id = int(thread_id) + except: + thread_id = False + found_routes = [(fallback_model, thread_id, DEFAULT_CONTACT_SETTINGS, custom_values, uid)] + _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) + + # Post-process routes + valid_routes = [] + author_id = False + for route in found_routes: + model, thread_id, contact = route[0], route[1], route[2] + model_pool = self.pool.get(model) + + # check related document effectively exists + if thread_id and not model_pool.exists(cr, uid, thread_id): + _logger.warning('Routing mail with Message-Id %s: route %s: reply to missing document %s, ignoring and creating new document instead', + message_id, route, thread_id) + thread_id = None + # check model accepts the mailgateway + if not (thread_id and hasattr(model_pool, 'message_update')) and not (hasattr(model_pool, 'message_new')): + _logger.warning('Routing mail with Message-Id %s: route %s: model %s does not accept unknown emails, skipping', + message_id, route, model) + continue + + # check for author + if not author_id: + email_from = decode(message.get('From')) + author_ids = model_pool._find_partner_from_emails(cr, uid, thread_id, [email_from], context=context) + if author_ids: + author_id = author_ids[0] + message_dict['author_id'] = author_id + + # route contact settings + if contact == 'followers' and thread_id: + obj = self.browse(cr, uid, id, context=context) + if not author_id or not author_id in obj.message_follower_ids: + _logger.warning('Routing mail with Message-Id %s: route %s: alias %s restricted to internal followers, skipping', + message_id, route, alias.alias_name) + create_bounce_email() + avoid_assert = True + continue + elif (contact == 'followers' or contact == 'partners') and not author_id: + _logger.warning('Routing mail with Message-Id %s: route %s: alias %s does not accept unknown sender, skipping', + message_id, route, alias.alias_name) + create_bounce_email() + avoid_assert = True + continue + + # update route + valid_routes.append((model, thread_id, route[3], route[4])) + + # AssertionError if no routes found and if no bounce occured + assert valid_routes or avoid_assert, \ + "Routing mail from %s to %s with Message-Id %s: no possible route found. " \ + "Create an appropriate mail.alias or force the destination model." % \ + (email_from, email_to, message_id) + return valid_routes def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, @@ -684,24 +789,20 @@ 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', []) - thread_id = False + routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context) for model, thread_id, custom_values, user_id in routes: if self._name == 'mail.thread': context.update({'thread_model': model}) @@ -713,11 +814,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." @@ -851,7 +951,6 @@ class mail_thread(osv.AbstractModel): """ msg_dict = { 'type': 'email', - 'author_id': False, } if not isinstance(message, Message): if isinstance(message, unicode): @@ -874,12 +973,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] @@ -933,7 +1027,7 @@ class mail_thread(osv.AbstractModel): partner_id, partner_name 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 @@ -961,59 +1055,6 @@ 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. - - 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) - - def message_find_partner_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 """ - mail_message_obj = self.pool.get('mail.message') - partner_obj = self.pool.get('res.partner') - 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] - 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), - ('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_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, content_subtype='html', **kwargs): @@ -1060,18 +1101,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) -- cgit v1.2.3 From c1a04891566456e18d869a9e0c55759f84d02d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 16 Apr 2013 15:44:05 +0200 Subject: [FIX] Chatter: fixed calls to refactored methos. bzr revid: tde@openerp.com-20130416134405-nxtwbvxbz77pxufx --- addons/mail/mail_thread.py | 1 - 1 file changed, 1 deletion(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index e9b43d41f72..f143b71dd83 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -705,7 +705,6 @@ class mail_thread(osv.AbstractModel): # check for author if not author_id: - email_from = decode(message.get('From')) author_ids = model_pool._find_partner_from_emails(cr, uid, thread_id, [email_from], context=context) if author_ids: author_id = author_ids[0] -- cgit v1.2.3 From a68e7fe9eb236bdc604ac7f7dfc2b9651ebf2a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 16 Apr 2013 17:38:09 +0200 Subject: [FIX] mail_thread: message_process: fixed body of bounced emails. bzr revid: tde@openerp.com-20130416153809-d5xbjqw6i7l7x4av --- addons/mail/mail_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index f143b71dd83..05354287284 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -618,7 +618,7 @@ class mail_thread(osv.AbstractModel): 'body_html': '

Hello,

' '

The following email sent to %s cannot be accepted because this address' 'is private. Only known contacts are allowed to contact this address.

' - '
%s
' % (message.get('to'), message.get('body')), + '
%s
' % (message.get('to'), message_dict.get('body')), 'subject': 'Re: %s' % message.get('subject'), 'email_to': message.get('from'), 'auto_delete': True, -- cgit v1.2.3 From a576c23043bdb8fee8bdeed250d9126dcf9b3495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 7 May 2013 15:01:49 +0200 Subject: [FIX] empty list menu tip: do not display 'False' aliases. bzr revid: tde@openerp.com-20130507130149-qd1rjfrphin6ds59 --- addons/mail/mail_thread.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 05354287284..547d2d4336b 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -101,18 +101,19 @@ 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) - alias = object_id.alias_id + if object_id.alias_id and object_id.alias_id.alias_name: # avoid void aliases + 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)], context=context, limit=1, order='id ASC') + alias_ids = alias_obj.search(cr, uid, [("alias_model_id", "=", model_id), ("alias_name", "!=", False)], context=context, limit=1, order='id ASC') if alias_ids: alias = alias_obj.browse(cr, uid, alias_ids[0], context=context) if alias: alias_email = alias.name_get()[0][1] return _("""

- Click here to add a new %(document)s or send an email to: %(email)s + Click here to add new %(document)s or send an email to: %(email)s

%(static_help)s""" ) % { @@ -122,7 +123,7 @@ class mail_thread(osv.AbstractModel): } if document_name != 'document' and help and help.find("oe_view_nocontent_create") == -1: - return _("

Click here to add a new %(document)s

%(static_help)s") % { + return _("

Click here to add new %(document)s

%(static_help)s") % { 'document': document_name, 'static_help': help or '', } -- cgit v1.2.3 From 88e3505554038350ae0274e9359bbffff342b9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 16 May 2013 18:42:07 +0200 Subject: [IMP] mail_alias: added reference to the parent record. The parent record is the record that holds the alias. It is different from the record updated or created using the alias. For example projects have aliases whose parent is the given projet, and that creates new task inside the project. Updated alias creation and management accordingly. This ownership concept is used to compute the 'followers' contact settings of aliases. bzr revid: tde@openerp.com-20130516164207-1zr0q6abw6a34ndf --- addons/mail/mail_thread.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 547d2d4336b..3151846bc04 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -594,7 +594,6 @@ class mail_thread(osv.AbstractModel): """ assert isinstance(message, Message), 'message must be an email.message.Message at this point' found_routes = [] - DEFAULT_CONTACT_SETTINGS = 'everyone' fallback_model = model avoid_assert = False @@ -636,7 +635,7 @@ class mail_thread(osv.AbstractModel): 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) - found_routes = [(model, thread_id, DEFAULT_CONTACT_SETTINGS, custom_values, uid)] + found_routes = [(model, thread_id, custom_values, uid, None)] # 2. Reply to a private message if not found_routes and in_reply_to: @@ -648,7 +647,7 @@ 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) - found_routes = [(message.model, message.res_id, DEFAULT_CONTACT_SETTINGS, custom_values, uid)] + found_routes = [(message.model, message.res_id, custom_values, uid, None)] # 3. Look for a matching mail.alias entry local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)] @@ -665,8 +664,7 @@ 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) - found_routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \ - alias.alias_contact, eval(alias.alias_defaults), user_id)) + found_routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)) if found_routes: _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r', email_from, email_to, message_id, found_routes) @@ -682,7 +680,7 @@ class mail_thread(osv.AbstractModel): thread_id = int(thread_id) except: thread_id = False - found_routes = [(fallback_model, thread_id, DEFAULT_CONTACT_SETTINGS, custom_values, uid)] + found_routes = [(fallback_model, thread_id, custom_values, uid, 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, fallback_model, thread_id, custom_values, uid) @@ -690,7 +688,7 @@ class mail_thread(osv.AbstractModel): valid_routes = [] author_id = False for route in found_routes: - model, thread_id, contact = route[0], route[1], route[2] + model, thread_id, alias = route[0], route[1], route[4] model_pool = self.pool.get(model) # check related document effectively exists @@ -712,15 +710,19 @@ class mail_thread(osv.AbstractModel): message_dict['author_id'] = author_id # route contact settings - if contact == 'followers' and thread_id: - obj = self.browse(cr, uid, id, context=context) + if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id): + print alias, alias.alias_contact + 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 obj.message_follower_ids: _logger.warning('Routing mail with Message-Id %s: route %s: alias %s restricted to internal followers, skipping', message_id, route, alias.alias_name) create_bounce_email() avoid_assert = True continue - elif (contact == 'followers' or contact == 'partners') and not author_id: + elif alias and alias.alias_contact == 'partners' and not author_id: _logger.warning('Routing mail with Message-Id %s: route %s: alias %s does not accept unknown sender, skipping', message_id, route, alias.alias_name) create_bounce_email() @@ -728,7 +730,7 @@ class mail_thread(osv.AbstractModel): continue # update route - valid_routes.append((model, thread_id, route[3], route[4])) + valid_routes.append((model, thread_id, route[2], route[3])) # AssertionError if no routes found and if no bounce occured assert valid_routes or avoid_assert, \ -- cgit v1.2.3 From ac3f8a7615e5c7664319cd57c4f832f63517a542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 17 May 2013 17:13:09 +0200 Subject: [FIX] mail_thread: fixed bug in contact check, comparing int to list of browse is not a good idea. bzr revid: tde@openerp.com-20130517151309-v0x32829dpwr0dw6 --- addons/mail/mail_thread.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 3151846bc04..b29dd1cbad6 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -616,8 +616,8 @@ class mail_thread(osv.AbstractModel): 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 address' - 'is private. Only known contacts are allowed to contact this address.

' + '

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'), @@ -711,12 +711,11 @@ class mail_thread(osv.AbstractModel): # route contact settings if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id): - print alias, alias.alias_contact 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 obj.message_follower_ids: + if not author_id or not author_id in [fol.id for fol in obj.message_follower_ids]: _logger.warning('Routing mail with Message-Id %s: route %s: alias %s restricted to internal followers, skipping', message_id, route, alias.alias_name) create_bounce_email() -- cgit v1.2.3 From fd90140d7bd05820aeac7bd7e22539893f87319d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 24 Jun 2013 17:18:27 +0200 Subject: [REF] mail_thread: refactored message_route improvement, to be closer to the original code + call to a new method that checks the route, instead of doing everything in one method. bzr revid: tde@openerp.com-20130624151827-hxwaelefs540pvca --- addons/mail/mail_thread.py | 310 ++++++++++++++++++++++++++------------------- 1 file changed, 177 insertions(+), 133 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index b4ce06812a9..409f0ad12f7 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 @@ -258,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 @@ -462,9 +460,6 @@ class mail_thread(osv.AbstractModel): return [('message_unread', '=', True)] return [] - # def _get_model(self, cr, uid, context=None): - # return if self._name == 'mail.thread' - def _garbage_collect_attachments(self, cr, uid, context=None): """ Garbage collect lost mail attachments. Those are attachments - linked to res_model 'mail.compose.message', the composer wizard @@ -597,6 +592,118 @@ class mail_thread(osv.AbstractModel): s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)]) 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 + """ + + 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' + + 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).' + _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, @@ -616,8 +723,8 @@ class mail_thread(osv.AbstractModel): provided. 4. If all the above fails, raise an exception. - :param dict message_dict: dictionary holding message variables :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) @@ -628,12 +735,10 @@ 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' - found_routes = [] fallback_model = model - avoid_assert = False # Get email.message.Message variables for future processing message_id = message.get('Message-Id') @@ -642,26 +747,6 @@ class mail_thread(osv.AbstractModel): references = decode_header(message, 'References') in_reply_to = decode_header(message, 'In-Reply-To') thread_references = references or in_reply_to - # 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 = ','.join([decode_header(message, 'Delivered-To'), - decode_header(message, 'To'), - decode_header(message, 'Cc'), - decode_header(message, 'Resent-To'), - decode_header(message, 'Resent-Cc')]) - - 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) # 1. Reply to an existing thread ref_match = thread_references and tools.reference_re.search(thread_references) @@ -673,10 +758,13 @@ class mail_thread(osv.AbstractModel): 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) - found_routes = [(model, thread_id, custom_values, uid, None)] + 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 not found_routes and in_reply_to: + if in_reply_to: message_ids = self.pool.get('mail.message').search(cr, uid, [ ('message_id', '=', in_reply_to), '!', ('message_id', 'ilike', 'reply_to') @@ -685,96 +773,67 @@ 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) - found_routes = [(message.model, message.res_id, custom_values, uid, None)] + 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 [] # 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 = ','.join([decode_header(message, 'Delivered-To'), + decode_header(message, 'To'), + decode_header(message, 'Cc'), + decode_header(message, 'Resent-To'), + decode_header(message, 'Resent-Cc')]) local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)] - if not found_routes and local_parts: + if local_parts: mail_alias = self.pool.get('mail.alias') alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)]) - for alias in mail_alias.browse(cr, uid, alias_ids, context=context): - user_id = alias.alias_user_id.id - if not user_id: - # TDE note: this could cause crashes, because no clue that the user - # that send the email has the right to create or modify a new document - # Fallback on user_id = uid - # Note: recognized partners will be added as followers anyway - # user_id = self._message_find_user_id(cr, uid, message, context=context) - user_id = uid - _logger.info('No matching user_id for the alias %s', alias.alias_name) - found_routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)) - if found_routes: - _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r', - email_from, email_to, message_id, found_routes) + if alias_ids: + routes = [] + for alias in mail_alias.browse(cr, uid, alias_ids, context=context): + user_id = alias.alias_user_id.id + if not user_id: + # TDE note: this could cause crashes, because no clue that the user + # that send the email has the right to create or modify a new document + # Fallback on user_id = uid + # Note: recognized partners will be added as followers anyway + # user_id = self._message_find_user_id(cr, uid, message, context=context) + user_id = uid + _logger.info('No matching user_id for the alias %s', alias.alias_name) + 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 # 4. Fallback to the provided parameters, if they work - if not found_routes: - if not thread_id: - # Legacy: fallback to matching [ID] in the Subject - match = tools.res_re.search(decode_header(message, 'Subject')) - thread_id = match and match.group(1) - # Convert into int (bug spotted in 7.0 because of str) - try: - thread_id = int(thread_id) - except: - thread_id = False - found_routes = [(fallback_model, thread_id, custom_values, uid, 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, fallback_model, thread_id, custom_values, uid) - - # Post-process routes - valid_routes = [] - author_id = False - for route in found_routes: - model, thread_id, alias = route[0], route[1], route[4] - model_pool = self.pool.get(model) - - # check related document effectively exists - if thread_id and not model_pool.exists(cr, uid, thread_id): - _logger.warning('Routing mail with Message-Id %s: route %s: reply to missing document %s, ignoring and creating new document instead', - message_id, route, thread_id) - thread_id = None - # check model accepts the mailgateway - if not (thread_id and hasattr(model_pool, 'message_update')) and not (hasattr(model_pool, 'message_new')): - _logger.warning('Routing mail with Message-Id %s: route %s: model %s does not accept unknown emails, skipping', - message_id, route, model) - continue - - # check for author - if not author_id: - author_ids = model_pool._find_partner_from_emails(cr, uid, thread_id, [email_from], context=context) - if author_ids: - author_id = author_ids[0] - message_dict['author_id'] = author_id - - # route 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]: - _logger.warning('Routing mail with Message-Id %s: route %s: alias %s restricted to internal followers, skipping', - message_id, route, alias.alias_name) - create_bounce_email() - avoid_assert = True - continue - elif alias and alias.alias_contact == 'partners' and not author_id: - _logger.warning('Routing mail with Message-Id %s: route %s: alias %s does not accept unknown sender, skipping', - message_id, route, alias.alias_name) - create_bounce_email() - avoid_assert = True - continue - - # update route - valid_routes.append((model, thread_id, route[2], route[3])) + if not thread_id: + # Legacy: fallback to matching [ID] in the Subject + match = tools.res_re.search(decode_header(message, 'Subject')) + thread_id = match and match.group(1) + # Convert into int (bug spotted in 7.0 because of str) + try: + thread_id = int(thread_id) + except: + thread_id = False + _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 valid_routes or avoid_assert, \ + assert False, \ "Routing mail from %s to %s with Message-Id %s: no possible route found. " \ "Create an appropriate mail.alias or force the destination model." % \ (email_from, email_to, message_id) - return valid_routes def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, @@ -840,9 +899,9 @@ class mail_thread(osv.AbstractModel): return False # find possible routes for the message - thread_id = False routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context) - for model, thread_id, custom_values, user_id in routes: + 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: @@ -1097,8 +1156,7 @@ 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 _find_partner_from_emails(self, cr, uid, id, emails, context=None, - check_followers=True, only_users=False, take_all=False, full_info=False): + def _find_partner_from_emails(self, cr, uid, id, emails, model=None, context=None, check_followers=True): """ Utility method to find partners :param boolean check_followers: TODO @@ -1108,8 +1166,11 @@ class mail_thread(osv.AbstractModel): partner_obj = self.pool['res.partner'] partner_ids = [] obj = None - if id and self._name != 'mail.thread' and check_followers: - obj = self.browse(cr, uid, id, context=context) + 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) @@ -1145,9 +1206,7 @@ class mail_thread(osv.AbstractModel): 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_ids = self._find_partner_from_emails(cr, uid, id, emails, context=context) result = list() @@ -1326,21 +1385,6 @@ class mail_thread(osv.AbstractModel): self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context) 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 #------------------------------------------------------ -- cgit v1.2.3 From e6ee1d315e21a377c104aa0bf05cd9a15b0ce844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 24 Jun 2013 18:24:57 +0200 Subject: [CLEAN] Misc cleaning before merging bzr revid: tde@openerp.com-20130624162457-8y8eqwebwolvcdc6 --- addons/mail/mail_thread.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 409f0ad12f7..6e01ea1069d 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -781,11 +781,12 @@ class mail_thread(osv.AbstractModel): # 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 = ','.join([decode_header(message, 'Delivered-To'), - decode_header(message, 'To'), - decode_header(message, 'Cc'), - decode_header(message, 'Resent-To'), - decode_header(message, 'Resent-Cc')]) + rcpt_tos = \ + ','.join([decode_header(message, 'Delivered-To'), + decode_header(message, 'To'), + decode_header(message, 'Cc'), + decode_header(message, 'Resent-To'), + decode_header(message, 'Resent-Cc')]) local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)] if local_parts: mail_alias = self.pool.get('mail.alias') @@ -831,9 +832,8 @@ class mail_thread(osv.AbstractModel): # AssertionError if no routes found and if no bounce occured assert False, \ - "Routing mail from %s to %s with Message-Id %s: no possible route found. " \ - "Create an appropriate mail.alias or force the destination model." % \ - (email_from, email_to, message_id) + "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) def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, -- cgit v1.2.3 From 3994ea12e9ca32604736db34f89faf777f6a18e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 25 Jun 2013 11:26:06 +0200 Subject: [CLEAN] Misc cleaning before merging (some code cleaning, added comments). bzr revid: tde@openerp.com-20130625092606-l8bqt3ce2xz4zm9t --- addons/mail/mail_thread.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 6e01ea1069d..403cfab7d61 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -746,9 +746,9 @@ class mail_thread(osv.AbstractModel): email_to = decode_header(message, 'To') references = decode_header(message, 'References') in_reply_to = decode_header(message, 'In-Reply-To') - thread_references = references or in_reply_to - # 1. Reply to an existing thread + # 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)) @@ -1157,11 +1157,15 @@ class mail_thread(osv.AbstractModel): return result def _find_partner_from_emails(self, cr, uid, id, emails, model=None, context=None, check_followers=True): - """ Utility method to find partners - - :param boolean check_followers: TODO - :param boolean only_users: TODO - :param boolean take_all: TODO + """ 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 + + :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 = [] -- cgit v1.2.3