1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
24 from openerp import tools
26 from email.header import decode_header
27 from openerp import SUPERUSER_ID, api
28 from openerp.osv import osv, orm, fields
29 from openerp.tools import html_email_clean
30 from openerp.tools.translate import _
31 from HTMLParser import HTMLParser
33 _logger = logging.getLogger(__name__)
36 from mako.template import Template as MakoTemplate
38 _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
41 """ Some tools for parsing / creating email fields """
43 """Returns unicode() string conversion of the the given encoded smtp header text"""
45 text = decode_header(text.replace('\r', ''))
46 return ''.join([tools.ustr(x[0], x[1]) for x in text])
48 class MLStripper(HTMLParser):
52 def handle_data(self, d):
55 return ''.join(self.fed)
62 class mail_message(osv.Model):
63 """ Messages model: system notification (replacing res.log notifications),
64 comments (OpenChatter discussion) and incoming emails. """
65 _name = 'mail.message'
66 _description = 'Message'
67 _inherit = ['ir.needaction_mixin']
69 _rec_name = 'record_name'
71 _message_read_limit = 30
72 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
73 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
74 _message_record_name_length = 18
75 _message_read_more_limit = 1024
77 def default_get(self, cr, uid, fields, context=None):
78 # protection for `default_type` values leaking from menu action context (e.g. for invoices)
79 if context and context.get('default_type') and context.get('default_type') not in [
80 val[0] for val in self._columns['type'].selection]:
81 context = dict(context, default_type=None)
82 return super(mail_message, self).default_get(cr, uid, fields, context=context)
84 def _get_to_read(self, cr, uid, ids, name, arg, context=None):
85 """ Compute if the message is unread by the current user. """
86 res = dict((id, False) for id in ids)
87 partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
88 notif_obj = self.pool.get('mail.notification')
89 notif_ids = notif_obj.search(cr, uid, [
90 ('partner_id', 'in', [partner_id]),
91 ('message_id', 'in', ids),
92 ('is_read', '=', False),
94 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
95 res[notif.message_id.id] = True
98 def _search_to_read(self, cr, uid, obj, name, domain, context=None):
99 """ Search for messages to read by the current user. Condition is
100 inversed because we search unread message on a is_read column. """
101 return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.is_read', '=', not domain[0][2])]
103 def _get_starred(self, cr, uid, ids, name, arg, context=None):
104 """ Compute if the message is unread by the current user. """
105 res = dict((id, False) for id in ids)
106 partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
107 notif_obj = self.pool.get('mail.notification')
108 notif_ids = notif_obj.search(cr, uid, [
109 ('partner_id', 'in', [partner_id]),
110 ('message_id', 'in', ids),
111 ('starred', '=', True),
113 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
114 res[notif.message_id.id] = True
117 def _search_starred(self, cr, uid, obj, name, domain, context=None):
118 """ Search for starred messages by the current user."""
119 return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
122 'type': fields.selection([
124 ('comment', 'Comment'),
125 ('notification', 'System notification'),
127 help="Message type: email for email message, notification for system "\
128 "message, comment for other messages such as user replies"),
129 'email_from': fields.char('From',
130 help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
131 'reply_to': fields.char('Reply-To',
132 help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
133 'same_thread': fields.boolean('Same thread',
134 help='Redirect answers to the same discussion thread.'),
135 'author_id': fields.many2one('res.partner', 'Author', select=1,
137 help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
138 'author_avatar': fields.related('author_id', 'image_small', type="binary", string="Author's Avatar"),
139 'partner_ids': fields.many2many('res.partner', string='Recipients'),
140 'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
141 'message_id', 'partner_id', 'Notified partners',
142 help='Partners that have a notification pushing this message in their mailboxes'),
143 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
144 'message_id', 'attachment_id', 'Attachments'),
145 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True,
146 ondelete='set null', help="Initial thread message."),
147 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
148 'model': fields.char('Related Document Model', size=128, select=1),
149 'res_id': fields.integer('Related Document ID', select=1),
150 'record_name': fields.char('Message Record Name', help="Name get of the related document."),
151 'notification_ids': fields.one2many('mail.notification', 'message_id',
152 string='Notifications', auto_join=True,
153 help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
154 'subject': fields.char('Subject'),
155 'date': fields.datetime('Date'),
156 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1, copy=False),
157 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
158 'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
159 type='boolean', string='To read',
160 help='Current user has an unread notification linked to this message'),
161 'starred': fields.function(_get_starred, fnct_search=_search_starred,
162 type='boolean', string='Starred',
163 help='Current user has a starred notification linked to this message'),
164 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
165 ondelete='set null', select=1,),
166 'vote_user_ids': fields.many2many('res.users', 'mail_vote',
167 'message_id', 'user_id', string='Votes',
168 help='Users that voted for this message'),
169 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
172 def _needaction_domain_get(self, cr, uid, context=None):
173 return [('to_read', '=', True)]
175 def _get_default_from(self, cr, uid, context=None):
176 this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
177 if this.alias_name and this.alias_domain:
178 return '%s <%s@%s>' % (this.name, this.alias_name, this.alias_domain)
180 return '%s <%s>' % (this.name, this.email)
181 raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
183 def _get_default_author(self, cr, uid, context=None):
184 return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
188 'date': fields.datetime.now,
189 'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
191 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
195 #------------------------------------------------------
197 #------------------------------------------------------
199 def vote_toggle(self, cr, uid, ids, context=None):
200 ''' Toggles vote. Performed using read to avoid access rights issues.
201 Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
202 for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
203 new_has_voted = not (uid in message.get('vote_user_ids'))
205 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
207 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
208 return new_has_voted or False
210 #------------------------------------------------------
211 # download an attachment
212 #------------------------------------------------------
214 def download_attachment(self, cr, uid, id_message, attachment_id, context=None):
215 """ Return the content of linked attachments. """
216 # this will fail if you cannot read the message
217 message_values = self.read(cr, uid, [id_message], ['attachment_ids'], context=context)[0]
218 if attachment_id in message_values['attachment_ids']:
219 attachment = self.pool.get('ir.attachment').browse(cr, SUPERUSER_ID, attachment_id, context=context)
220 if attachment.datas and attachment.datas_fname:
222 'base64': attachment.datas,
223 'filename': attachment.datas_fname,
227 #------------------------------------------------------
229 #------------------------------------------------------
231 @api.cr_uid_ids_context
232 def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, context=None):
233 """ Set messages as (un)read. Technically, the notifications related
234 to uid are set to (un)read. If for some msg_ids there are missing
235 notifications (i.e. due to load more or thread parent fetching),
238 :param bool read: set notification as (un)read
239 :param bool create_missing: create notifications for missing entries
240 (i.e. when acting on displayed messages not notified)
242 :return number of message mark as read
244 notification_obj = self.pool.get('mail.notification')
245 user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
246 domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
247 if not create_missing:
248 domain += [('is_read', '=', not read)]
249 notif_ids = notification_obj.search(cr, uid, domain, context=context)
251 # all message have notifications: already set them as (un)read
252 if len(notif_ids) == len(msg_ids) or not create_missing:
253 notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
254 return len(notif_ids)
256 # some messages do not have notifications: find which one, create notification, update read status
257 notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
258 to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
259 for msg_id in to_create_msg_ids:
260 notification_obj.create(cr, uid, {'partner_id': user_pid, 'is_read': read, 'message_id': msg_id}, context=context)
261 notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
262 return len(notif_ids)
264 @api.cr_uid_ids_context
265 def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
266 """ Set messages as (un)starred. Technically, the notifications related
267 to uid are set to (un)starred.
269 :param bool starred: set notification as (un)starred
270 :param bool create_missing: create notifications for missing entries
271 (i.e. when acting on displayed messages not notified)
273 notification_obj = self.pool.get('mail.notification')
274 user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
275 domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
276 if not create_missing:
277 domain += [('starred', '=', not starred)]
282 values['is_read'] = False
284 notif_ids = notification_obj.search(cr, uid, domain, context=context)
286 # all message have notifications: already set them as (un)starred
287 if len(notif_ids) == len(msg_ids) or not create_missing:
288 notification_obj.write(cr, uid, notif_ids, values, context=context)
291 # some messages do not have notifications: find which one, create notification, update starred status
292 notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
293 to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
294 for msg_id in to_create_msg_ids:
295 notification_obj.create(cr, uid, dict(values, partner_id=user_pid, message_id=msg_id), context=context)
296 notification_obj.write(cr, uid, notif_ids, values, context=context)
299 #------------------------------------------------------
300 # Message loading for web interface
301 #------------------------------------------------------
303 def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
304 """ Post-processing on values given by message_read. This method will
305 handle partners in batch to avoid doing numerous queries.
307 :param list messages: list of message, as get_dict result
308 :param dict message_tree: {[msg.id]: msg browse record}
310 res_partner_obj = self.pool.get('res.partner')
311 ir_attachment_obj = self.pool.get('ir.attachment')
312 pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
314 # 1. Aggregate partners (author_id and partner_ids) and attachments
316 attachment_ids = set()
317 for key, message in message_tree.iteritems():
318 if message.author_id:
319 partner_ids |= set([message.author_id.id])
320 if message.subtype_id and message.notified_partner_ids: # take notified people of message with a subtype
321 partner_ids |= set([partner.id for partner in message.notified_partner_ids])
322 elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log)
323 partner_ids |= set([partner.id for partner in message.partner_ids])
324 if message.attachment_ids:
325 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
326 # Read partners as SUPERUSER -> display the names like classic m2o even if no access
327 partners = res_partner_obj.name_get(cr, SUPERUSER_ID, list(partner_ids), context=context)
328 partner_tree = dict((partner[0], partner) for partner in partners)
330 # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
331 attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type_icon'], context=context)
332 attachments_tree = dict((attachment['id'], {
333 'id': attachment['id'],
334 'filename': attachment['datas_fname'],
335 'name': attachment['name'],
336 'file_type_icon': attachment['file_type_icon'],
337 }) for attachment in attachments)
339 # 3. Update message dictionaries
340 for message_dict in messages:
341 message_id = message_dict.get('id')
342 message = message_tree[message_id]
343 if message.author_id:
344 author = partner_tree[message.author_id.id]
346 author = (0, message.email_from)
348 if message.subtype_id:
349 partner_ids = [partner_tree[partner.id] for partner in message.notified_partner_ids
350 if partner.id in partner_tree]
352 partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
353 if partner.id in partner_tree]
355 for attachment in message.attachment_ids:
356 if attachment.id in attachments_tree:
357 attachment_ids.append(attachments_tree[attachment.id])
358 message_dict.update({
359 'is_author': pid == author[0],
361 'partner_ids': partner_ids,
362 'attachment_ids': attachment_ids,
367 def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
368 """ Return a dict representation of the message. This representation is
369 used in the JS client code, to display the messages. Partners and
370 attachments related stuff will be done in post-processing in batch.
372 :param dict message: mail.message browse record
374 # private message: no model, no res_id
376 if not message.model or not message.res_id:
378 # votes and favorites: res.users ids, no prefetching should be done
379 vote_nb = len(message.vote_user_ids)
380 has_voted = uid in [user.id for user in message.vote_user_ids]
387 body_short = html_email_clean(message.body, remove=False, shorten=True, max_length=max_length)
390 body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
391 _logger.exception(Exception)
393 return {'id': message.id,
394 'type': message.type,
395 'subtype': message.subtype_id.name if message.subtype_id else False,
396 'body': message.body,
397 'body_short': body_short,
398 'model': message.model,
399 'res_id': message.res_id,
400 'record_name': message.record_name,
401 'subject': message.subject,
402 'date': message.date,
403 'to_read': message.to_read,
404 'parent_id': parent_id,
405 'is_private': is_private,
407 'author_avatar': message.author_avatar,
411 'has_voted': has_voted,
412 'is_favorite': message.starred,
413 'attachment_ids': [],
416 def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
417 message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
418 """ Create expandables for message_read, to load new messages.
419 1. get the expandable for new threads
420 if display is flat (thread_level == 0):
421 fetch message_ids < min(already displayed ids), because we
422 want a flat display, ordered by id
424 fetch message_ids that are not childs of already displayed
426 2. get the expandables for new messages inside threads if display
428 for each thread header, search for its childs
429 for each hole in the child list based on message displayed,
432 :param list messages: list of message structure for the Chatter
433 widget to which expandables are added
434 :param dict message_tree: dict [id]: browse record of this message
435 :param dict parent_tree: dict [parent_id]: [child_ids]
436 :param list message_unload_ids: list of message_ids we do not want
440 def _get_expandable(domain, message_nb, parent_id, max_limit):
443 'nb_messages': message_nb,
444 'type': 'expandable',
445 'parent_id': parent_id,
446 'max_limit': max_limit,
451 message_ids = sorted(message_tree.keys())
453 # 1. get the expandable for new threads
454 if thread_level == 0:
455 exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
457 exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
458 ids = self.search(cr, uid, exp_domain, context=context, limit=1)
460 # inside a thread: prepend
462 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
463 # new threads: append
465 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
467 # 2. get the expandables for new messages inside threads if display is not flat
468 if thread_level == 0:
470 for message_id in message_ids:
471 message = message_tree[message_id]
473 # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
474 if message.parent_id:
477 # check there are message for expandable
478 child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
479 child_ids = sorted(list(child_ids), reverse=True)
483 # make groups of unread messages
484 id_min, id_max, nb = max(child_ids), 0, 0
485 for child_id in child_ids:
486 if not child_id in message_ids:
488 if id_min > child_id:
490 if id_max < child_id:
493 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
494 idx = [msg.get('id') for msg in messages].index(child_id) + 1
495 # messages.append(_get_expandable(exp_domain, nb, message_id, False))
496 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
497 id_min, id_max, nb = max(child_ids), 0, 0
499 id_min, id_max, nb = max(child_ids), 0, 0
501 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
502 idx = [msg.get('id') for msg in messages].index(message_id) + 1
503 # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
504 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
509 def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
510 thread_level=0, context=None, parent_id=False, limit=None):
511 """ Read messages from mail.message, and get back a list of structured
512 messages to be displayed as discussion threads. If IDs is set,
513 fetch these records. Otherwise use the domain to fetch messages.
514 After having fetch messages, their ancestors will be added to obtain
515 well formed threads, if uid has access to them.
517 After reading the messages, expandable messages are added in the
518 message list (see ``_message_read_add_expandables``). It consists
519 in messages holding the 'read more' data: number of messages to
520 read, domain to apply.
522 :param list ids: optional IDs to fetch
523 :param list domain: optional domain for searching ids if ids not set
524 :param list message_unload_ids: optional ids we do not want to fetch,
525 because i.e. they are already displayed somewhere
526 :param int parent_id: context of parent_id
527 - if parent_id reached when adding ancestors, stop going further
528 in the ancestor search
529 - if set in flat mode, ancestor_id is set to parent_id
530 :param int limit: number of messages to fetch, before adding the
531 ancestors and expandables
532 :return list: list of message structure for the Chatter widget
534 assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
535 domain = domain if domain is not None else []
536 message_unload_ids = message_unload_ids if message_unload_ids is not None else []
537 if message_unload_ids:
538 domain += [('id', 'not in', message_unload_ids)]
539 limit = limit or self._message_read_limit
544 # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
546 ids = self.search(cr, uid, domain, context=context, limit=limit)
548 # fetch parent if threaded, sort messages
549 for message in self.browse(cr, uid, ids, context=context):
550 message_id = message.id
551 if message_id in message_tree:
553 message_tree[message_id] = message
556 if thread_level == 0:
557 tree_parent_id = parent_id
559 tree_parent_id = message_id
561 while parent.parent_id and parent.parent_id.id != parent_id:
562 parent = parent.parent_id
563 tree_parent_id = parent.id
564 if not parent.id in message_tree:
565 message_tree[parent.id] = parent
566 # newest messages first
567 parent_tree.setdefault(tree_parent_id, [])
568 if tree_parent_id != message_id:
569 parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
572 for key, message_id_list in parent_tree.iteritems():
573 message_id_list.sort(key=lambda item: item['id'])
574 message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
576 # create final ordered message_list based on parent_tree
577 parent_list = parent_tree.items()
578 parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True)
579 message_list = [message for (key, msg_list) in parent_list for message in msg_list]
581 # get the child expandable messages for the tree
582 self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
583 self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
584 thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
587 #------------------------------------------------------
588 # mail_message internals
589 #------------------------------------------------------
592 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
593 if not cr.fetchone():
594 cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
596 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
597 doc_ids = doc_dict.keys()
598 allowed_doc_ids = self.pool[doc_model].search(cr, uid, [('id', 'in', doc_ids)], context=context)
599 return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
601 def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
602 model_access_obj = self.pool.get('ir.model.access')
604 for doc_model, doc_dict in model_ids.iteritems():
605 if not model_access_obj.check(cr, uid, doc_model, 'read', False):
607 allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
610 def _search(self, cr, uid, args, offset=0, limit=None, order=None,
611 context=None, count=False, access_rights_uid=None):
612 """ Override that adds specific access rights of mail.message, to remove
613 ids uid could not see according to our custom rules. Please refer
614 to check_access_rule for more details about those rules.
616 After having received ids of a classic search, keep only:
617 - if author_id == pid, uid is the author, OR
618 - a notification (id, pid) exists, uid has been notified, OR
619 - uid have read access to the related document is model, res_id
620 - otherwise: remove the id
622 # Rules do not apply to administrator
623 if uid == SUPERUSER_ID:
624 return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
625 context=context, count=count, access_rights_uid=access_rights_uid)
626 # Perform a super with count as False, to have the ids, not a counter
627 ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
628 context=context, count=False, access_rights_uid=access_rights_uid)
629 if not ids and count:
634 pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
635 author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
638 messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
639 for message in messages:
640 if message.get('author_id') and message.get('author_id')[0] == pid:
641 author_ids.add(message.get('id'))
642 elif pid in message.get('notified_partner_ids'):
643 partner_ids.add(message.get('id'))
644 elif message.get('model') and message.get('res_id'):
645 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
647 allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
648 final_ids = author_ids | partner_ids | allowed_ids
651 return len(final_ids)
653 # re-construct a list based on ids, because set did not keep the original order
654 id_list = [id for id in ids if id in final_ids]
657 def check_access_rule(self, cr, uid, ids, operation, context=None):
658 """ Access rules of mail.message:
660 - author_id == pid, uid is the author, OR
661 - mail_notification (id, pid) exists, uid has been notified, OR
662 - uid have read access to the related document if model, res_id
665 - no model, no res_id, I create a private message OR
666 - pid in message_follower_ids if model, res_id OR
667 - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
668 - uid have write or create access on the related document if model, res_id, OR
671 - author_id == pid, uid is the author, OR
672 - uid has write or create access on the related document if model, res_id
675 - uid has write or create access on the related document if model, res_id
678 def _generate_model_record_ids(msg_val, msg_ids):
679 """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
680 :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
682 model_record_ids = {}
684 vals = msg_val.get(id, {})
685 if vals.get('model') and vals.get('res_id'):
686 model_record_ids.setdefault(vals['model'], set()).add(vals['res_id'])
687 return model_record_ids
689 if uid == SUPERUSER_ID:
691 if isinstance(ids, (int, long)):
693 not_obj = self.pool.get('mail.notification')
694 fol_obj = self.pool.get('mail.followers')
695 partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
697 # Read mail_message.ids to have their values
698 message_values = dict.fromkeys(ids, {})
699 cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
700 for id, rmod, rid, author_id, parent_id in cr.fetchall():
701 message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
703 # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
705 if operation == 'read' or operation == 'write':
706 author_ids = [mid for mid, message in message_values.iteritems()
707 if message.get('author_id') and message.get('author_id') == partner_id]
708 elif operation == 'create':
709 author_ids = [mid for mid, message in message_values.iteritems()
710 if not message.get('model') and not message.get('res_id')]
712 # Parent condition, for create (check for received notifications for the created message parent)
714 if operation == 'create':
715 parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
716 if message.get('parent_id')]
717 not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
718 not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
719 notified_ids += [mid for mid, message in message_values.iteritems()
720 if message.get('parent_id') in not_parent_ids]
722 # Notification condition, for read (check for received notifications and create (in message_follower_ids)) -> could become an ir.rule, but not till we do not have a many2one variable field
723 other_ids = set(ids).difference(set(author_ids), set(notified_ids))
724 model_record_ids = _generate_model_record_ids(message_values, other_ids)
725 if operation == 'read':
726 not_ids = not_obj.search(cr, SUPERUSER_ID, [
727 ('partner_id', '=', partner_id),
728 ('message_id', 'in', ids),
730 notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
731 elif operation == 'create':
732 for doc_model, doc_ids in model_record_ids.items():
733 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
734 ('res_model', '=', doc_model),
735 ('res_id', 'in', list(doc_ids)),
736 ('partner_id', '=', partner_id),
738 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
739 notified_ids += [mid for mid, message in message_values.iteritems()
740 if message.get('model') == doc_model and message.get('res_id') in fol_mids]
742 # CRUD: Access rights related to the document
743 other_ids = other_ids.difference(set(notified_ids))
744 model_record_ids = _generate_model_record_ids(message_values, other_ids)
745 document_related_ids = []
746 for model, doc_ids in model_record_ids.items():
747 model_obj = self.pool[model]
748 mids = model_obj.exists(cr, uid, list(doc_ids))
749 if hasattr(model_obj, 'check_mail_message_access'):
750 model_obj.check_mail_message_access(cr, uid, mids, operation, context=context)
752 self.pool['mail.thread'].check_mail_message_access(cr, uid, mids, operation, model_obj=model_obj, context=context)
753 document_related_ids += [mid for mid, message in message_values.iteritems()
754 if message.get('model') == model and message.get('res_id') in mids]
756 # Calculate remaining ids: if not void, raise an error
757 other_ids = other_ids.difference(set(document_related_ids))
760 raise orm.except_orm(_('Access Denied'),
761 _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
762 (self._description, operation))
764 def _get_record_name(self, cr, uid, values, context=None):
765 """ Return the related document name, using name_get. It is done using
766 SUPERUSER_ID, to be sure to have the record name correctly stored. """
767 if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
769 return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
771 def _get_reply_to(self, cr, uid, values, context=None):
772 """ Return a specific reply_to: alias of the document through message_get_reply_to
773 or take the email_from
775 model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
776 ctx = dict(context, thread_model=model)
777 return self.pool['mail.thread'].message_get_reply_to(cr, uid, [res_id], default=email_from, context=ctx)[res_id]
779 def _get_message_id(self, cr, uid, values, context=None):
780 if values.get('same_thread', True) is False:
781 message_id = tools.generate_tracking_message_id('reply_to')
782 elif values.get('res_id') and values.get('model'):
783 message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
785 message_id = tools.generate_tracking_message_id('private')
788 def create(self, cr, uid, values, context=None):
789 context = dict(context or {})
790 default_starred = context.pop('default_starred', False)
792 if 'email_from' not in values: # needed to compute reply_to
793 values['email_from'] = self._get_default_from(cr, uid, context=context)
794 if 'message_id' not in values:
795 values['message_id'] = self._get_message_id(cr, uid, values, context=context)
796 if 'reply_to' not in values:
797 values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
798 if 'record_name' not in values and 'default_record_name' not in context:
799 values['record_name'] = self._get_record_name(cr, uid, values, context=context)
801 newid = super(mail_message, self).create(cr, uid, values, context)
803 self._notify(cr, uid, newid, context=context,
804 force_send=context.get('mail_notify_force_send', True),
805 user_signature=context.get('mail_notify_user_signature', True))
806 # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
807 # Because starred will call set_message_starred, that looks for notifications.
808 # When creating a new mail_message, it will create a notification to a message
809 # that does not exist, leading to an error (key not existing). Also this
810 # this means unread notifications will be created, yet we can not assure
811 # this is what we want.
813 self.set_message_starred(cr, uid, [newid], True, context=context)
816 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
817 """ Override to explicitely call check_access_rule, that is not called
818 by the ORM. It instead directly fetches ir.rules and apply them. """
819 self.check_access_rule(cr, uid, ids, 'read', context=context)
820 res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
823 def unlink(self, cr, uid, ids, context=None):
824 # cascade-delete attachments that are directly attached to the message (should only happen
825 # for mail.messages that act as parent for a standalone mail.mail record).
826 self.check_access_rule(cr, uid, ids, 'unlink', context=context)
827 attachments_to_delete = []
828 for message in self.browse(cr, uid, ids, context=context):
829 for attach in message.attachment_ids:
830 if attach.res_model == self._name and (attach.res_id == message.id or attach.res_id == 0):
831 attachments_to_delete.append(attach.id)
832 if attachments_to_delete:
833 self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
834 return super(mail_message, self).unlink(cr, uid, ids, context=context)
836 #------------------------------------------------------
838 #------------------------------------------------------
840 def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
841 """ Add the related record followers to the destination partner_ids if is not a private message.
842 Call mail_notification.notify to manage the email sending
844 notification_obj = self.pool.get('mail.notification')
845 message = self.browse(cr, uid, newid, context=context)
846 partners_to_notify = set([])
848 # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message)
849 if message.subtype_id and message.model and message.res_id:
850 fol_obj = self.pool.get("mail.followers")
851 # browse as SUPERUSER because rules could restrict the search results
852 fol_ids = fol_obj.search(
854 ('res_model', '=', message.model),
855 ('res_id', '=', message.res_id),
857 partners_to_notify |= set(
858 fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
859 if message.subtype_id.id in [st.id for st in fo.subtype_ids]
861 # remove me from notified partners, unless the message is written on my own wall
862 if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
863 partners_to_notify |= set([message.author_id.id])
864 elif message.author_id:
865 partners_to_notify -= set([message.author_id.id])
867 # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
868 if message.partner_ids:
869 partners_to_notify |= set([p.id for p in message.partner_ids])
872 notification_obj._notify(
873 cr, uid, newid, partners_to_notify=list(partners_to_notify), context=context,
874 force_send=force_send, user_signature=user_signature
878 # An error appear when a user receive a notification without notifying
879 # the parent message -> add a read notification for the parent
880 if message.parent_id:
881 # all notified_partner_ids of the mail.message have to be notified for the parented messages
882 partners_to_parent_notify = set(message.notified_partner_ids).difference(message.parent_id.notified_partner_ids)
883 for partner in partners_to_parent_notify:
884 notification_obj.create(cr, uid, {
885 'message_id': message.parent_id.id,
886 'partner_id': partner.id,