##############################################################################
import base64
+from collections import OrderedDict
import datetime
import dateutil
import email
import xmlrpclib
import re
from email.message import Message
+from urllib import urlencode
from openerp import tools
from openerp import SUPERUSER_ID
# :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. """
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)
+ if not alias and catchall_domain and model: # no res_id or res_id not linked to an alias -> generic help message, take a generic alias of the model
alias_obj = self.pool.get('mail.alias')
- alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False)], context=context, order='id ASC')
+ alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False), ('alias_parent_thread_id', '=', False)], context=context, order='id ASC')
if alias_ids and len(alias_ids) == 1:
alias = alias_obj.browse(cr, uid, alias_ids[0], context=context)
# 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()
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['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'):
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((item, False) for item in 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
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 = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
+
+ tracked_fields = None
+ if not context.get('mail_notrack'):
+ tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
+
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)
self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context, values=values)
- if not context.get('mail_notrack'):
- # Perform the tracking
- tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
- else:
- tracked_fields = None
+ # 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):
: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
_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,
thread_references = references or in_reply_to
# 1. message is a reply to an existing message (exact match of message_id)
+ 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:
reply_thread_id = int(ref_match.group(1))
reply_model = ref_match.group(2) or fallback_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 []
+ 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]
- # 2. Reply to a private message
+ # 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 []
-
- # 3. Look for a matching mail.alias entry
+ 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]
+
+ # 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
+ # 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.' %
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)
if set(partner_ids) == set([user_pid]):
try:
self.check_access_rights(cr, uid, 'read')
- if context.get('operation', '') == 'create':
- self.check_access_rule(cr, uid, ids, 'create')
- else:
- self.check_access_rule(cr, uid, ids, 'read')
+ self.check_access_rule(cr, uid, ids, 'read')
except (osv.except_osv, orm.except_orm):
return False
else:
}
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