summaryrefslogtreecommitdiff
path: root/addons/mail/mail_thread.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/mail_thread.py')
-rw-r--r--addons/mail/mail_thread.py561
1 files changed, 380 insertions, 181 deletions
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index a46f416f44b..94e9126f020 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
@@ -71,6 +70,7 @@ class mail_thread(osv.AbstractModel):
_name = 'mail.thread'
_description = 'Email Thread'
_mail_flat_thread = True
+ _mail_post_access = 'write'
# Automatic logging system if mail installed
# _track = {
@@ -102,21 +102,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 +127,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 '',
}
@@ -156,12 +157,26 @@ class mail_thread(osv.AbstractModel):
res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New"))
return res
- def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
+ def read_followers_data(self, cr, uid, follower_ids, context=None):
+ result = []
+ technical_group = self.pool.get('ir.model.data').get_object(cr, uid, 'base', 'group_no_one')
+ for follower in self.pool.get('res.partner').browse(cr, uid, follower_ids, context=context):
+ is_editable = uid in map(lambda x: x.id, technical_group.users)
+ is_uid = uid in map(lambda x: x.id, follower.user_ids)
+ data = (follower.id,
+ follower.name,
+ {'is_editable': is_editable, 'is_uid': is_uid},
+ )
+ result.append(data)
+ return result
+
+ def _get_subscription_data(self, cr, uid, ids, name, args, user_pid=None, context=None):
""" Computes:
- message_subtype_data: data about document subtypes: which are
available, which are followed if any """
res = dict((id, dict(message_subtype_data='')) for id in ids)
- user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ if user_pid is None:
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
# find current model subtypes, add them to a dictionary
subtype_obj = self.pool.get('mail.message.subtype')
@@ -240,21 +255,41 @@ class mail_thread(osv.AbstractModel):
fol_obj.create(cr, SUPERUSER_ID, {'res_model': self._name, 'res_id': id, 'partner_id': partner_id})
def _search_followers(self, cr, uid, obj, name, args, context):
+ """Search function for message_follower_ids
+
+ Do not use with operator 'not in'. Use instead message_is_followers
+ """
fol_obj = self.pool.get('mail.followers')
res = []
for field, operator, value in args:
assert field == name
+ # TOFIX make it work with not in
+ assert operator != "not in", "Do not search message_follower_ids with 'not in'"
fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)])
res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)]
res.append(('id', 'in', res_ids))
return res
+ def _search_is_follower(self, cr, uid, obj, name, args, context):
+ """Search function for message_is_follower"""
+ res = []
+ for field, operator, value in args:
+ assert field == name
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ if (operator == '=' and value) or (operator == '!=' and not value): # is a follower
+ res_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
+ else: # is not a follower or unknown domain
+ mail_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
+ res_ids = self.search(cr, uid, [('id', 'not in', mail_ids)], context=context)
+ res.append(('id', 'in', res_ids))
+ return res
+
_columns = {
- 'message_is_follower': fields.function(_get_followers,
- type='boolean', string='Is a Follower', multi='_get_followers,'),
+ 'message_is_follower': fields.function(_get_followers, type='boolean',
+ fnct_search=_search_is_follower, string='Is a Follower', multi='_get_followers,'),
'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
- fnct_search=_search_followers, type='many2many',
- obj='res.partner', string='Followers', multi='_get_followers'),
+ fnct_search=_search_followers, type='many2many',
+ obj='res.partner', string='Followers', multi='_get_followers'),
'message_ids': fields.one2many('mail.message', 'res_id',
domain=lambda self: [('model', '=', self._name)],
auto_join=True,
@@ -292,7 +327,12 @@ class mail_thread(osv.AbstractModel):
# subscribe uid unless asked not to
if not context.get('mail_create_nosubscribe'):
self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
- self.message_auto_subscribe(cr, uid, [thread_id], values.keys(), context=context)
+ # auto_subscribe: take values and defaults into account
+ create_values = set(values.keys())
+ for key, val in context.iteritems():
+ if key.startswith('default_'):
+ create_values.add(key[8:])
+ self.message_auto_subscribe(cr, uid, [thread_id], list(create_values), context=context)
# track values
tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context)
@@ -308,8 +348,8 @@ class mail_thread(osv.AbstractModel):
# Track initial values of tracked fields
tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context)
if tracked_fields:
- initial = self.read(cr, uid, ids, tracked_fields.keys(), context=context)
- initial_values = dict((item['id'], item) for item in initial)
+ records = self.browse(cr, uid, ids, context=context)
+ initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)
# Perform write, update followers
result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
@@ -368,7 +408,7 @@ class mail_thread(osv.AbstractModel):
if not value:
return ''
if col_info['type'] == 'many2one':
- return value[1]
+ return value.name_get()[0][1]
if col_info['type'] == 'selection':
return dict(col_info['selection'])[value]
return value
@@ -387,23 +427,26 @@ class mail_thread(osv.AbstractModel):
if not tracked_fields:
return True
- for record in self.read(cr, uid, ids, tracked_fields.keys(), context=context):
- initial = initial_values[record['id']]
- changes = []
+ for browse_record in self.browse(cr, uid, ids, context=context):
+ initial = initial_values[browse_record.id]
+ changes = set()
tracked_values = {}
# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name, col_info in tracked_fields.items():
- if record[col_name] == initial[col_name] and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
+ initial_value = initial[col_name]
+ record_value = getattr(browse_record, col_name)
+
+ if record_value == initial_value and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
tracked_values[col_name] = dict(col_info=col_info['string'],
- new_value=convert_for_display(record[col_name], col_info))
- elif record[col_name] != initial[col_name]:
+ new_value=convert_for_display(record_value, col_info))
+ elif record_value != initial_value and (record_value or initial_value): # because browse null != False
if getattr(self._all_columns[col_name].column, 'track_visibility', None) in ['always', 'onchange']:
tracked_values[col_name] = dict(col_info=col_info['string'],
- old_value=convert_for_display(initial[col_name], col_info),
- new_value=convert_for_display(record[col_name], col_info))
+ old_value=convert_for_display(initial_value, col_info),
+ new_value=convert_for_display(record_value, col_info))
if col_name in tracked_fields:
- changes.append(col_name)
+ changes.add(col_name)
if not changes:
continue
@@ -413,7 +456,7 @@ class mail_thread(osv.AbstractModel):
if field not in changes:
continue
for subtype, method in track_info.items():
- if method(self, cr, uid, record, context):
+ if method(self, cr, uid, browse_record, context):
subtypes.append(subtype)
posted = False
@@ -424,11 +467,11 @@ class mail_thread(osv.AbstractModel):
_logger.debug('subtype %s not found, giving error "%s"' % (subtype, e))
continue
message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
- self.message_post(cr, uid, record['id'], body=message, subtype=subtype, context=context)
+ self.message_post(cr, uid, browse_record.id, body=message, subtype=subtype, context=context)
posted = True
if not posted:
message = format_message('', tracked_values)
- self.message_post(cr, uid, record['id'], body=message, context=context)
+ self.message_post(cr, uid, browse_record.id, body=message, context=context)
return True
#------------------------------------------------------
@@ -467,12 +510,22 @@ class mail_thread(osv.AbstractModel):
access rule on the document, for portal document such as issues. """
if not model_obj:
model_obj = self
- if operation in ['create', 'write', 'unlink']:
- model_obj.check_access_rights(cr, uid, 'write')
- model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
+ if hasattr(self, '_mail_post_access'):
+ create_allow = self._mail_post_access
+ else:
+ create_allow = 'write'
+
+ if operation in ['write', 'unlink']:
+ check_operation = 'write'
+ elif operation == 'create' and create_allow in ['create', 'read', 'write', 'unlink']:
+ check_operation = create_allow
+ elif operation == 'create':
+ check_operation = 'write'
else:
- model_obj.check_access_rights(cr, uid, operation)
- model_obj.check_access_rule(cr, uid, mids, operation, context=context)
+ check_operation = operation
+
+ model_obj.check_access_rights(cr, uid, check_operation)
+ model_obj.check_access_rule(cr, uid, mids, check_operation, context=context)
def _get_formview_action(self, cr, uid, id, model=None, context=None):
""" Return an action to open the document. This method is meant to be
@@ -543,6 +596,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'])
@@ -559,34 +614,136 @@ class mail_thread(osv.AbstractModel):
ret_dict = {}
for model_name in self.pool.obj_list():
model = self.pool[model_name]
- if 'mail.thread' in getattr(model, '_inherit', []):
+ if hasattr(model, "message_process") and hasattr(model, "message_post"):
ret_dict[model_name] = model._description
return ret_dict
def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None):
""" Find partners related to some header fields of the message.
- TDE TODO: merge me with other partner finding methods in 8.0 """
- partner_obj = self.pool.get('res.partner')
- partner_ids = []
+ :param string message: an email.message instance """
s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
- for email_address in tools.email_split(s):
- related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context)
- if not related_partners:
- related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address)], limit=1, context=context)
- partner_ids += related_partners
- return partner_ids
+ return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context))
+
+ def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None):
+ """ Verify route validity. Check and rules:
+ 1 - if thread_id -> check that document effectively exists; otherwise
+ fallback on a message_new by resetting thread_id
+ 2 - check that message_update exists if thread_id is set; or at least
+ that message_new exist
+ [ - find author_id if udpate_author is set]
+ 3 - if there is an alias, check alias_contact:
+ 'followers' and thread_id:
+ check on target document that the author is in the followers
+ 'followers' and alias_parent_thread_id:
+ check on alias parent document that the author is in the
+ followers
+ 'partners': check that author_id id set
+ """
- def _message_find_user_id(self, cr, uid, message, context=None):
- """ TDE TODO: check and maybe merge me with other user finding methods in 8.0 """
- from_local_part = tools.email_split(decode(message.get('From')))[0]
- # FP Note: canonification required, the minimu: .lower()
- user_ids = self.pool.get('res.users').search(cr, uid, ['|',
- ('login', '=', from_local_part),
- ('email', '=', from_local_part)], context=context)
- return user_ids[0] if user_ids else uid
+ assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
+ assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
+
+ 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
+ # 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).'
+ _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
+ _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
- def message_route(self, cr, uid, message, model=None, thread_id=None,
+ # 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.
@@ -606,6 +763,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)
@@ -616,9 +774,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')
@@ -628,30 +789,35 @@ 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, [
+ mail_message_ids = self.pool.get('mail.message').search(cr, uid, [
('message_id', '=', in_reply_to),
'!', ('message_id', 'ilike', 'reply_to')
], limit=1, context=context)
- if message_ids:
- message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
+ if mail_message_ids:
+ mail_message = self.pool.get('mail.message').browse(cr, uid, mail_message_ids[0], context=context)
_logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, message.id, custom_values, uid)
- return [(message.model, message.res_id, custom_values, uid)]
+ email_from, email_to, message_id, mail_message.id, custom_values, uid)
+ route = self.message_route_verify(cr, uid, message, message_dict,
+ (mail_message.model, mail_message.res_id, custom_values, uid, None),
+ update_author=True, assert_model=True, create_fallback=True, context=context)
+ return route and [route] or []
- # 2. Look for a matching mail.alias entry
+ # 3. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
rcpt_tos = \
@@ -676,14 +842,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'))
@@ -693,16 +861,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,
@@ -756,25 +926,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:
@@ -785,15 +951,17 @@ 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."
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', **msg)
if partner_ids:
@@ -923,7 +1091,6 @@ class mail_thread(osv.AbstractModel):
"""
msg_dict = {
'type': 'email',
- 'author_id': False,
}
if not isinstance(message, Message):
if isinstance(message, unicode):
@@ -946,12 +1113,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]
@@ -1005,7 +1167,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
@@ -1033,53 +1195,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:
@@ -1128,22 +1313,11 @@ class mail_thread(osv.AbstractModel):
model = False
if thread_id:
model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
- if model != self._name:
+ if model != self._name and hasattr(self.pool[model], 'message_post'):
del context['thread_model']
return self.pool[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)
@@ -1255,27 +1429,12 @@ 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
#------------------------------------------------------
- def message_get_subscription_data(self, cr, uid, ids, context=None):
+ def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
""" Wrapper to get subtypes data. """
- return self._get_subscription_data(cr, uid, ids, None, None, context=context)
+ return self._get_subscription_data(cr, uid, ids, None, None, user_pid=user_pid, context=context)
def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
""" Wrapper on message_subscribe, using users. If user_ids is not
@@ -1287,7 +1446,10 @@ class mail_thread(osv.AbstractModel):
def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None):
""" Add partners to the records followers. """
- user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ mail_followers_obj = self.pool.get('mail.followers')
+ subtype_obj = self.pool.get('mail.message.subtype')
+
+ user_pid = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
if set(partner_ids) == set([user_pid]):
try:
self.check_access_rights(cr, uid, 'read')
@@ -1296,27 +1458,35 @@ class mail_thread(osv.AbstractModel):
else:
self.check_access_rights(cr, uid, 'write')
- # subscribe partners
- self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
-
- # subtype specified: update the subscriptions
- fol_obj = self.pool.get('mail.followers')
- if subtype_ids is not None:
- fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context)
- fol_obj.write(cr, SUPERUSER_ID, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
- # no subtypes: default ones for new subscription, do not update existing subscriptions
- else:
- # search new subscriptions: subtype_ids is False
- fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
- ('res_model', '=', self._name),
- ('res_id', 'in', ids),
- ('partner_id', 'in', partner_ids),
- ('subtype_ids', '=', False)
- ], context=context)
- if fol_ids:
- subtype_obj = self.pool.get('mail.message.subtype')
- subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
- fol_obj.write(cr, SUPERUSER_ID, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
+ for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
+ existing_pids = set([f.id for f in record.message_follower_ids
+ if f.id in partner_ids])
+ new_pids = set(partner_ids) - existing_pids
+
+ # subtype_ids specified: update already subscribed partners
+ if subtype_ids and existing_pids:
+ fol_ids = mail_followers_obj.search(cr, SUPERUSER_ID, [
+ ('res_model', '=', self._name),
+ ('res_id', '=', record.id),
+ ('partner_id', 'in', list(existing_pids)),
+ ], context=context)
+ mail_followers_obj.write(cr, SUPERUSER_ID, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
+ # subtype_ids not specified: do not update already subscribed partner, fetch default subtypes for new partners
+ elif subtype_ids is None:
+ subtype_ids = subtype_obj.search(cr, uid, [
+ ('default', '=', True),
+ '|',
+ ('res_model', '=', self._name),
+ ('res_model', '=', False)
+ ], context=context)
+ # subscribe new followers
+ for new_pid in new_pids:
+ mail_followers_obj.create(cr, SUPERUSER_ID, {
+ 'res_model': self._name,
+ 'res_id': record.id,
+ 'partner_id': new_pid,
+ 'subtype_ids': [(6, 0, subtype_ids)],
+ }, context=context)
return True
@@ -1454,4 +1624,33 @@ class mail_thread(osv.AbstractModel):
''', (ids, self._name, partner_id))
return True
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+ #------------------------------------------------------
+ # Thread suggestion
+ #------------------------------------------------------
+
+ def get_suggested_thread(self, cr, uid, removed_suggested_threads=None, context=None):
+ """Return a list of suggested threads, sorted by the numbers of followers"""
+ if context is None:
+ context = {}
+
+ # TDE HACK: originally by MAT from portal/mail_mail.py but not working until the inheritance graph bug is not solved in trunk
+ # TDE FIXME: relocate in portal when it won't be necessary to reload the hr.employee model in an additional bridge module
+ if self.pool['res.groups']._all_columns.get('is_portal'):
+ user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
+ if any(group.is_portal for group in user.groups_id):
+ return []
+
+ threads = []
+ if removed_suggested_threads is None:
+ removed_suggested_threads = []
+
+ thread_ids = self.search(cr, uid, [('id', 'not in', removed_suggested_threads), ('message_is_follower', '=', False)], context=context)
+ for thread in self.browse(cr, uid, thread_ids, context=context):
+ data = {
+ 'id': thread.id,
+ 'popularity': len(thread.message_follower_ids),
+ 'name': thread.name,
+ 'image_small': thread.image_small
+ }
+ threads.append(data)
+ return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]