##############################################################################
import base64
+from collections import OrderedDict
import datetime
import dateutil
import email
from lxml import etree
import logging
import pytz
+import socket
import time
import xmlrpclib
+import re
from email.message import Message
+from urllib import urlencode
from openerp import tools
from openerp import SUPERUSER_ID
_logger = logging.getLogger(__name__)
+mail_header_msgid_re = re.compile('<[^<>]+>')
+
def decode_header(message, header, separator=' '):
return separator.join(map(decode, filter(None, message.get_all(header, []))))
# :param function lambda: returns whether the tracking should record using this subtype
_track = {}
+ # Mass mailing feature
+ _mail_mass_mailing = False
+
def get_empty_list_help(self, cr, uid, help, context=None):
""" Override of BaseModel.get_empty_list_help() to generate an help message
that adds alias information. """
model = context.get('empty_list_help_model')
res_id = context.get('empty_list_help_id')
ir_config_parameter = self.pool.get("ir.config_parameter")
- catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
+ catchall_domain = ir_config_parameter.get_param(cr, SUPERUSER_ID, "mail.catchall.domain", context=context)
document_name = context.get('empty_list_help_document_name', _('document'))
alias = None
if res[id]['message_unread_count']:
title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
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"))
+ res[id].pop('message_unread_count', None)
return res
def read_followers_data(self, cr, uid, follower_ids, context=None):
# find current model subtypes, add them to a dictionary
subtype_obj = self.pool.get('mail.message.subtype')
- subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
- subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False, id=subtype.id)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
+ subtype_ids = subtype_obj.search(
+ cr, uid, [
+ '&', ('hidden', '=', False), '|', ('res_model', '=', self._name), ('res_model', '=', False)
+ ], context=context)
+ subtype_dict = OrderedDict(
+ (subtype.name, {
+ 'default': subtype.default,
+ 'followed': False,
+ 'parent_model': subtype.parent_id and subtype.parent_id.res_model or self._name,
+ 'id': subtype.id}
+ ) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
for id in ids:
res[id]['message_subtype_data'] = subtype_dict.copy()
], context=context)
for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
thread_subtype_dict = res[fol.res_id]['message_subtype_data']
- for subtype in fol.subtype_ids:
+ for subtype in [st for st in fol.subtype_ids if st.name in thread_subtype_dict]:
thread_subtype_dict[subtype.name]['followed'] = True
res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
res = []
for field, operator, value in args:
assert field == name
- partner_id = self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0]
+ 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
auto_join=True,
string='Messages',
help="Messages and communication history"),
+ 'message_last_post': fields.datetime('Last Message Date',
+ help='Date of the last message posted on the record.'),
'message_unread': fields.function(_get_message_data,
fnct_search=_search_message_unread, multi="_get_message_data",
type='boolean', string='Unread Messages',
if context is None:
context = {}
+ if context.get('tracking_disable'):
+ return super(mail_thread, self).create(
+ cr, uid, values, context=context)
+
# subscribe uid unless asked not to
if not context.get('mail_create_nosubscribe'):
- pid = self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0]
+ pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid).partner_id.id
message_follower_ids = values.get('message_follower_ids') or [] # webclient can send None or False
message_follower_ids.append([4, pid])
values['message_follower_ids'] = message_follower_ids
- # add operation to ignore access rule checking for subscription
- context_operation = dict(context, operation='create')
- else:
- context_operation = context
- thread_id = super(mail_thread, self).create(cr, uid, values, context=context_operation)
+ thread_id = super(mail_thread, self).create(cr, uid, values, context=context)
# automatic logging unless asked not to (mainly for various testing purpose)
if not context.get('mail_create_nolog'):
- self.message_post(cr, uid, thread_id, body=_('%s created') % (self._description), context=context)
+ ir_model_pool = self.pool['ir.model']
+ ids = ir_model_pool.search(cr, uid, [('model', '=', self._name)], context=context)
+ name = ir_model_pool.read(cr, uid, ids, ['name'], context=context)[0]['name']
+ self.message_post(cr, uid, thread_id, body=_('%s created') % name, context=context)
# auto_subscribe: take values and defaults into account
create_values = dict(values)
track_ctx = dict(context)
if 'lang' not in track_ctx:
track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang
- tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
- if tracked_fields:
- initial_values = {thread_id: dict((item, False) for item in tracked_fields)}
- self.message_track(cr, uid, [thread_id], tracked_fields, initial_values, context=track_ctx)
+ if not context.get('mail_notrack'):
+ tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
+ if tracked_fields:
+ initial_values = {thread_id: dict.fromkeys(tracked_fields, False)}
+ self.message_track(cr, uid, [thread_id], tracked_fields, initial_values, context=track_ctx)
return thread_id
def write(self, cr, uid, ids, values, context=None):
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
+ if context.get('tracking_disable'):
+ return super(mail_thread, self).write(
+ cr, uid, ids, values, context=context)
# Track initial values of tracked fields
track_ctx = dict(context)
if 'lang' not in track_ctx:
track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang
+
+ tracked_fields = None
if not context.get('mail_notrack'):
tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
- else:
- tracked_fields = []
+
if tracked_fields:
records = self.browse(cr, uid, ids, context=track_ctx)
- initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)
+ initial_values = dict((record.id, dict((key, getattr(record, key)) for key in tracked_fields))
+ for record in records)
# Perform write, update followers
result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
# Perform the tracking
if tracked_fields:
self.message_track(cr, uid, ids, tracked_fields, initial_values, context=track_ctx)
+
return result
def unlink(self, cr, uid, ids, context=None):
fol_obj.unlink(cr, SUPERUSER_ID, fol_ids, context=context)
return res
- def copy(self, cr, uid, id, default=None, context=None):
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ # avoid tracking multiple temporary changes during copy
+ context = dict(context or {}, mail_notrack=True)
+
default = default or {}
default['message_ids'] = []
default['message_follower_ids'] = []
- return super(mail_thread, self).copy(cr, uid, id, default=default, context=context)
+ return super(mail_thread, self).copy_data(cr, uid, id, default=default, context=context)
#------------------------------------------------------
# Automatically log tracked fields
:return list: a list of (field_name, column_info obj), containing
always tracked fields and modified on_change fields
"""
- lst = []
+ tracked_fields = []
for name, column_info in self._all_columns.items():
visibility = getattr(column_info.column, 'track_visibility', False)
if visibility == 'always' or (visibility == 'onchange' and name in updated_fields) or name in self._track:
- lst.append(name)
- if not lst:
- return lst
- return self.fields_get(cr, uid, lst, context=context)
+ tracked_fields.append(name)
+
+ if tracked_fields:
+ return self.fields_get(cr, uid, tracked_fields, context=context)
+ return {}
def message_track(self, cr, uid, ids, tracked_fields, initial_values, context=None):
model_obj.check_access_rights(cr, uid, check_operation)
model_obj.check_access_rule(cr, uid, mids, check_operation, context=context)
- def _get_formview_action(self, cr, uid, id, model=None, context=None):
- """ Return an action to open the document. This method is meant to be
- overridden in addons that want to give specific view ids for example.
-
- :param int id: id of the document to open
- :param string model: specific model that overrides self._name
- """
- return {
- 'type': 'ir.actions.act_window',
- 'res_model': model or self._name,
- 'view_type': 'form',
- 'view_mode': 'form',
- 'views': [(False, 'form')],
- 'target': 'current',
- 'res_id': id,
- }
-
def _get_inbox_action_xml_id(self, cr, uid, context=None):
""" When redirecting towards the Inbox, choose which action xml_id has
to be fetched. This method is meant to be inherited, at least in portal
if model_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
try:
model_obj.check_access_rule(cr, uid, [res_id], 'read', context=context)
- if not hasattr(model_obj, '_get_formview_action'):
- action = self.pool.get('mail.thread')._get_formview_action(cr, uid, res_id, model=model, context=context)
- else:
- action = model_obj._get_formview_action(cr, uid, res_id, context=context)
+ action = model_obj.get_formview_action(cr, uid, res_id, context=context)
except (osv.except_osv, orm.except_orm):
pass
action.update({
})
return action
+ def _get_access_link(self, cr, uid, mail, partner, context=None):
+ # the parameters to encode for the query and fragment part of url
+ query = {'db': cr.dbname}
+ fragment = {
+ 'login': partner.user_ids[0].login,
+ 'action': 'mail.action_mail_redirect',
+ }
+ if mail.notification:
+ fragment['message_id'] = mail.mail_message_id.id
+ elif mail.model and mail.res_id:
+ fragment.update(model=mail.model, res_id=mail.res_id)
+
+ return "/web?%s#%s" % (urlencode(query), urlencode(fragment))
+
#------------------------------------------------------
# Email specific
#------------------------------------------------------
+ def message_get_default_recipients(self, cr, uid, ids, context=None):
+ if context and context.get('thread_model') and context['thread_model'] in self.pool and context['thread_model'] != self._name:
+ if hasattr(self.pool[context['thread_model']], 'message_get_default_recipients'):
+ sub_ctx = dict(context)
+ sub_ctx.pop('thread_model')
+ return self.pool[context['thread_model']].message_get_default_recipients(cr, uid, ids, context=sub_ctx)
+ res = {}
+ for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
+ recipient_ids, email_to, email_cc = set(), False, False
+ if 'partner_id' in self._all_columns and record.partner_id:
+ recipient_ids.add(record.partner_id.id)
+ elif 'email_from' in self._all_columns and record.email_from:
+ email_to = record.email_from
+ elif 'email' in self._all_columns:
+ email_to = record.email
+ res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc}
+ return res
+
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'])
- if record.get('alias_domain') and record.get('alias_name')
- else False
- for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)]
+ return ["%s@%s" % (record.alias_name, record.alias_domain)
+ if record.alias_domain and record.alias_name else False
+ for record in self.browse(cr, SUPERUSER_ID, ids, context=context)]
+
+ def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
+ """ Temporary method to create custom notification email values for a given
+ model and document. This should be better to have a headers field on
+ the mail.mail model, computed when creating the notification email, but
+ this cannot be done in a stable version.
+
+ TDE FIXME: rethink this ulgy thing. """
+ res = dict()
+ return res
#------------------------------------------------------
# Mail gateway
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):
+ def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False, 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
# 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), received %s.' % thread_id
- _warn('posting a message without model should be with a null res_id (private message), received %s, resetting thread_id' % thread_id)
+ if thread_id:
+ raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id)
+ _warn('posting a message without model should be with a null res_id (private message), received %s resetting thread_id' % thread_id)
thread_id = 0
# Private message: should have a parent_id (only answers)
if not model and not message_dict.get('parent_id'):
if assert_model:
- assert message_dict.get('parent_id'), 'Routing: posting a message without model should be with a parent_id (private mesage).'
+ if not message_dict.get('parent_id'):
+ raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).')
_warn('posting a message without model should be with a parent_id (private mesage), skipping')
return ()
# New Document: check model accepts the mailgateway
if not thread_id and model and not hasattr(model_pool, 'message_new'):
if assert_model:
- assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model
+ if not hasattr(model_pool, 'message_new'):
+ raise ValueError(
+ 'Model %s does not accept document creation, crashing' % model
+ )
_warn('model %s does not accept document creation, skipping' % model)
return ()
_create_bounce_email()
return ()
+ if not model and not thread_id and not alias and not allow_private:
+ 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,
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, alias]
+
+ :raises: ValueError, TypeError
"""
- assert isinstance(message, Message), 'message must be an email.message.Message at this point'
+ if not isinstance(message, Message):
+ raise TypeError('message must be an email.message.Message at this point')
mail_msg_obj = self.pool['mail.message']
fallback_model = model
thread_references = references or in_reply_to
# 1. message is a reply to an existing message (exact match of message_id)
- msg_references = thread_references.split()
+ ref_match = thread_references and tools.reference_re.search(thread_references)
+ msg_references = mail_header_msgid_re.findall(thread_references)
mail_message_ids = mail_msg_obj.search(cr, uid, [('message_id', 'in', msg_references)], context=context)
- if mail_message_ids:
+ if ref_match and mail_message_ids:
original_msg = mail_msg_obj.browse(cr, SUPERUSER_ID, mail_message_ids[0], context=context)
model, thread_id = original_msg.model, original_msg.res_id
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, model, thread_id, custom_values, uid)
route = self.message_route_verify(
cr, uid, message, message_dict,
(model, thread_id, custom_values, uid, None),
- update_author=True, assert_model=True, create_fallback=True, context=context)
- return route and [route] or []
+ update_author=True, assert_model=False, create_fallback=True, context=context)
+ if route:
+ _logger.info(
+ 'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ email_from, email_to, message_id, model, thread_id, custom_values, uid)
+ return [route]
# 2. message is a reply to an existign thread (6.1 compatibility)
- 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 fallback_model
- if thread_id and model in self.pool:
- model_obj = self.pool[model]
- compat_mail_msg_ids = mail_msg_obj.search(
- cr, uid, [
- ('message_id', '=', False),
- ('model', '=', model),
- ('res_id', '=', thread_id),
- ], context=context)
- if compat_mail_msg_ids and 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 thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, model, thread_id, custom_values, uid)
- route = self.message_route_verify(
- cr, uid, message, message_dict,
- (model, thread_id, custom_values, uid, None),
- update_author=True, assert_model=True, create_fallback=True, context=context)
- return route and [route] or []
-
- # 2. Reply to a private message
+ reply_thread_id = int(ref_match.group(1))
+ reply_model = ref_match.group(2) or fallback_model
+ reply_hostname = ref_match.group(3)
+ local_hostname = socket.gethostname()
+ # do not match forwarded emails from another OpenERP system (thread_id collision!)
+ if local_hostname == reply_hostname:
+ thread_id, model = reply_thread_id, reply_model
+ if thread_id and model in self.pool:
+ model_obj = self.pool[model]
+ compat_mail_msg_ids = mail_msg_obj.search(
+ cr, uid, [
+ ('message_id', '=', False),
+ ('model', '=', model),
+ ('res_id', '=', thread_id),
+ ], context=context)
+ if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
+ 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)
+ if route:
+ _logger.info(
+ 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ email_from, email_to, message_id, model, thread_id, custom_values, uid)
+ return [route]
+
+ # 3. Reply to a private message
if in_reply_to:
mail_message_ids = mail_msg_obj.search(cr, uid, [
('message_id', '=', in_reply_to),
], limit=1, context=context)
if mail_message_ids:
mail_message = mail_msg_obj.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, 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 []
+ update_author=True, assert_model=True, create_fallback=True, allow_private=True, context=context)
+ if route:
+ _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, mail_message.id, custom_values, uid)
+ return [route]
- # 3. Look for a matching mail.alias entry
+ # 4. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
rcpt_tos = \
user_id = 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:
+ _logger.info(
+ 'Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
+ email_from, email_to, message_id, route)
routes.append(route)
return routes
- # 4. Fallback to the provided parameters, if they work
+ # 5. Fallback to the provided parameters, if they work
if not thread_id:
# Legacy: fallback to matching [ID] in the Subject
match = tools.res_re.search(decode_header(message, 'Subject'))
thread_id = int(thread_id)
except:
thread_id = False
- _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:
+ _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)
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)
+ # ValueError if no routes found and if no bounce occured
+ raise ValueError(
+ 'No possible route found for incoming message from %s to %s (Message-Id %s:). '
+ 'Create an appropriate mail.alias or force the destination model.' %
+ (email_from, email_to, message_id)
+ )
def message_route_process(self, cr, uid, message, message_dict, routes, context=None):
# postpone setting message_dict.partner_ids after message_post, to avoid double notifications
context.update({'thread_model': model})
if model:
model_pool = self.pool[model]
- assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
- "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
- (message_dict['message_id'], model)
+ if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')):
+ raise ValueError(
+ "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" %
+ (message_dict['message_id'], model)
+ )
# disabled subscriptions during message_new/update to avoid having the system user running the
# email gateway become a follower of all inbound messages
else:
thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx)
else:
- assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
+ if thread_id:
+ raise ValueError("Posting a message without model should be with a null res_id, to create a private message.")
model_pool = self.pool.get('mail.thread')
if not hasattr(model_pool, 'message_post'):
context['thread_model'] = model
body = u''
if save_original:
attachments.append(('original_email.eml', message.as_string()))
- if not message.is_multipart() or 'text/' in message.get('content-type', ''):
+
+ # Be careful, content-type may contain tricky content like in the
+ # following example so test the MIME type with startswith()
+ #
+ # Content-Type: multipart/related;
+ # boundary="_004_3f1e4da175f349248b8d43cdeb9866f1AMSPR06MB343eurprd06pro_";
+ # type="text/html"
+ if not message.is_multipart() or message.get('content-type', '').startswith("text/"):
encoding = message.get_content_charset()
body = message.get_payload(decode=True)
body = tools.ustr(body, encoding, errors='replace')
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
if message.get('In-Reply-To'):
- parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
+ parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To'].strip()))])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
if message.get('References') and 'parent_id' not in msg_dict:
- parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
- [x.strip() for x in decode(message['References']).split()])])
+ msg_list = mail_header_msgid_re.findall(decode(message['References']))
+ parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', [x.strip() for x in msg_list])])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
return result
if partner and partner in obj.message_follower_ids: # recipient already in the followers -> skip
return result
- if partner and partner in [val[0] for val in result[obj.id]]: # already existing partner ID -> skip
+ if partner and partner.id in [val[0] for val in result[obj.id]]: # already existing partner ID -> skip
return result
if partner and partner.email: # complete profile: id, name <email>
result[obj.id].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
if subtype:
if '.' not in subtype:
subtype = 'mail.%s' % subtype
- ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
- subtype_id = ref and ref[1] or False
+ subtype_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, subtype)
# automatically subscribe recipients if asked to
if context.get('mail_post_autofollow') and thread_id and partner_ids:
for x in ('from', 'to', 'cc'):
values.pop(x, None)
- # Create and auto subscribe the author
+ # Post the message
msg_id = mail_message.create(cr, uid, values, context=context)
+
+ # Post-process: subscribe author, update message_last_post
+ if model and model != 'mail.thread' and thread_id and subtype_id:
+ # done with SUPERUSER_ID, because on some models users can post only with read access, not necessarily write access
+ self.write(cr, SUPERUSER_ID, [thread_id], {'message_last_post': fields.datetime.now()}, context=context)
message = mail_message.browse(cr, uid, msg_id, context=context)
if message.author_id and thread_id and type != 'notification' and not context.get('mail_create_nosubscribe'):
self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
""" Add partners to the records followers. """
if context is None:
context = {}
+ # not necessary for computation, but saves an access right check
+ if not partner_ids:
+ return True
mail_followers_obj = self.pool.get('mail.followers')
subtype_obj = self.pool.get('mail.message.subtype')
- user_pid = self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0]
+ user_pid = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
if set(partner_ids) == set([user_pid]):
- if context.get('operation', '') != 'create':
- try:
- self.check_access_rights(cr, uid, 'read')
- self.check_access_rule(cr, uid, ids, 'read')
- except (osv.except_osv, orm.except_orm):
- return False
+ try:
+ self.check_access_rights(cr, uid, 'read')
+ self.check_access_rule(cr, uid, ids, 'read')
+ except (osv.except_osv, orm.except_orm):
+ return False
else:
self.check_access_rights(cr, uid, 'write')
self.check_access_rule(cr, uid, ids, 'write')
def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
""" Remove partners from the records followers. """
+ # not necessary for computation, but saves an access right check
+ if not partner_ids:
+ return True
user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
if set(partner_ids) == set([user_pid]):
self.check_access_rights(cr, uid, 'read')
def message_mark_as_unread(self, cr, uid, ids, context=None):
""" Set as unread. """
- partner_id = self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0]
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
cr.execute('''
UPDATE mail_notification SET
read=false
def message_mark_as_read(self, cr, uid, ids, context=None):
""" Set as read. """
- partner_id = self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0]
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
cr.execute('''
UPDATE mail_notification SET
read=true
}
threads.append(data)
return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]
+
+ def message_change_thread(self, cr, uid, id, new_res_id, new_model, context=None):
+ """
+ Transfert the list of the mail thread messages from an model to another
+
+ :param id : the old res_id of the mail.message
+ :param new_res_id : the new res_id of the mail.message
+ :param new_model : the name of the new model of the mail.message
+
+ Example : self.pool.get("crm.lead").message_change_thread(self, cr, uid, 2, 4, "project.issue", context)
+ will transfert thread of the lead (id=2) to the issue (id=4)
+ """
+
+ # get the sbtype id of the comment Message
+ subtype_res_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'mail.mt_comment', raise_if_not_found=True)
+
+ # get the ids of the comment and none-comment of the thread
+ message_obj = self.pool.get('mail.message')
+ msg_ids_comment = message_obj.search(cr, uid, [
+ ('model', '=', self._name),
+ ('res_id', '=', id),
+ ('subtype_id', '=', subtype_res_id)], context=context)
+ msg_ids_not_comment = message_obj.search(cr, uid, [
+ ('model', '=', self._name),
+ ('res_id', '=', id),
+ ('subtype_id', '!=', subtype_res_id)], context=context)
+
+ # update the messages
+ message_obj.write(cr, uid, msg_ids_comment, {"res_id" : new_res_id, "model" : new_model}, context=context)
+ message_obj.write(cr, uid, msg_ids_not_comment, {"res_id" : new_res_id, "model" : new_model, "subtype_id" : None}, context=context)
+
+ return True