[MERGE] forward port of branch saas-5 up to e4cb520
[odoo/odoo.git] / addons / mail / mail_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
6 #
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
11 #
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
16 #
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/>
19 #
20 ##############################################################################
21
22 import logging
23
24 from openerp import tools
25
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
32
33 _logger = logging.getLogger(__name__)
34
35 try:
36     from mako.template import Template as MakoTemplate
37 except ImportError:
38     _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
39
40
41 """ Some tools for parsing / creating email fields """
42 def decode(text):
43     """Returns unicode() string conversion of the the given encoded smtp header text"""
44     if text:
45         text = decode_header(text.replace('\r', ''))
46         return ''.join([tools.ustr(x[0], x[1]) for x in text])
47
48 class MLStripper(HTMLParser):
49     def __init__(self):
50         self.reset()
51         self.fed = []
52     def handle_data(self, d):
53         self.fed.append(d)
54     def get_data(self):
55         return ''.join(self.fed)
56
57 def strip_tags(html):
58     s = MLStripper()
59     s.feed(html)
60     return s.get_data()
61
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']
68     _order = 'id desc'
69     _rec_name = 'record_name'
70
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
76
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)
83
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),
93         ], context=context)
94         for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
95             res[notif.message_id.id] = True
96         return res
97
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])]
102
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),
112         ], context=context)
113         for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
114             res[notif.message_id.id] = True
115         return res
116
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])]
120
121     _columns = {
122         'type': fields.selection([
123                         ('email', 'Email'),
124                         ('comment', 'Comment'),
125                         ('notification', 'System notification'),
126                         ], 'Type', size=12, 
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,
136             ondelete='set null',
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),
170     }
171
172     def _needaction_domain_get(self, cr, uid, context=None):
173         return [('to_read', '=', True)]
174
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)
179         elif this.email:
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."))
182
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
185
186     _defaults = {
187         'type': 'email',
188         'date': fields.datetime.now,
189         'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
190         'body': '',
191         'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
192         'same_thread': True,
193     }
194
195     #------------------------------------------------------
196     # Vote/Like
197     #------------------------------------------------------
198
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'))
204             if new_has_voted:
205                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
206             else:
207                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
208         return new_has_voted or False
209
210     #------------------------------------------------------
211     # download an attachment
212     #------------------------------------------------------
213
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:
221                 return {
222                     'base64': attachment.datas,
223                     'filename': attachment.datas_fname,
224                 }
225         return False
226
227     #------------------------------------------------------
228     # Notification API
229     #------------------------------------------------------
230
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),
236             they are created.
237
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)
241
242             :return number of message mark as read
243         """
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)
250
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)
255
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)
263
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.
268
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)
272         """
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)]
278         values = {
279             'starred': starred
280         }
281         if starred:
282             values['is_read'] = False
283
284         notif_ids = notification_obj.search(cr, uid, domain, context=context)
285
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)
289             return starred
290
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)
297         return starred
298
299     #------------------------------------------------------
300     # Message loading for web interface
301     #------------------------------------------------------
302
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.
306
307             :param list messages: list of message, as get_dict result
308             :param dict message_tree: {[msg.id]: msg browse record}
309         """
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
313
314         # 1. Aggregate partners (author_id and partner_ids) and attachments
315         partner_ids = set()
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)
329
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)
338
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]
345             else:
346                 author = (0, message.email_from)
347             partner_ids = []
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]
351             else:
352                 partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
353                                 if partner.id in partner_tree]
354             attachment_ids = []
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],
360                 'author_id': author,
361                 'partner_ids': partner_ids,
362                 'attachment_ids': attachment_ids,
363                 'user_pid': pid
364                 })
365         return True
366
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.
371
372             :param dict message: mail.message browse record
373         """
374         # private message: no model, no res_id
375         is_private = False
376         if not message.model or not message.res_id:
377             is_private = True
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]
381
382         try:
383             if parent_id:
384                 max_length = 300
385             else:
386                 max_length = 100
387             body_short = html_email_clean(message.body, remove=False, shorten=True, max_length=max_length)
388
389         except Exception:
390             body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
391             _logger.exception(Exception)
392
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,
406                 'author_id': False,
407                 'author_avatar': message.author_avatar,
408                 'is_author': False,
409                 'partner_ids': [],
410                 'vote_nb': vote_nb,
411                 'has_voted': has_voted,
412                 'is_favorite': message.starred,
413                 'attachment_ids': [],
414             }
415
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
423                 else:
424                     fetch message_ids that are not childs of already displayed
425                     messages
426             2. get the expandables for new messages inside threads if display
427                is not flat
428                 for each thread header, search for its childs
429                     for each hole in the child list based on message displayed,
430                     create an expandable
431
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
437                 to load
438             :return bool: True
439         """
440         def _get_expandable(domain, message_nb, parent_id, max_limit):
441             return {
442                 'domain': domain,
443                 'nb_messages': message_nb,
444                 'type': 'expandable',
445                 'parent_id': parent_id,
446                 'max_limit':  max_limit,
447             }
448
449         if not messages:
450             return True
451         message_ids = sorted(message_tree.keys())
452
453         # 1. get the expandable for new threads
454         if thread_level == 0:
455             exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
456         else:
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)
459         if ids:
460             # inside a thread: prepend
461             if parent_id:
462                 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
463             # new threads: append
464             else:
465                 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
466
467         # 2. get the expandables for new messages inside threads if display is not flat
468         if thread_level == 0:
469             return True
470         for message_id in message_ids:
471             message = message_tree[message_id]
472
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:
475                 continue
476
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)
480             if not child_ids:
481                 continue
482
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:
487                     nb += 1
488                     if id_min > child_id:
489                         id_min = child_id
490                     if id_max < child_id:
491                         id_max = child_id
492                 elif nb > 0:
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
498                 else:
499                     id_min, id_max, nb = max(child_ids), 0, 0
500             if nb > 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))
505
506         return True
507
508     @api.cr_uid_context
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.
516
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.
521
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
533         """
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
540         message_tree = {}
541         message_list = []
542         parent_tree = {}
543
544         # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
545         if ids is None:
546             ids = self.search(cr, uid, domain, context=context, limit=limit)
547
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:
552                 continue
553             message_tree[message_id] = message
554
555             # find parent_id
556             if thread_level == 0:
557                 tree_parent_id = parent_id
558             else:
559                 tree_parent_id = message_id
560                 parent = message
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))
570
571         if thread_level:
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))
575
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]
580
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)
585         return message_list
586
587     #------------------------------------------------------
588     # mail_message internals
589     #------------------------------------------------------
590
591     def init(self, cr):
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)""")
595
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]])
600
601     def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
602         model_access_obj = self.pool.get('ir.model.access')
603         allowed_ids = set()
604         for doc_model, doc_dict in model_ids.iteritems():
605             if not model_access_obj.check(cr, uid, doc_model, 'read', False):
606                 continue
607             allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
608         return allowed_ids
609
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.
615
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
621         """
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:
630             return 0
631         elif not ids:
632             return ids
633
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([])
636         model_ids = {}
637
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'))
646
647         allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
648         final_ids = author_ids | partner_ids | allowed_ids
649
650         if count:
651             return len(final_ids)
652         else:
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]
655             return id_list
656
657     def check_access_rule(self, cr, uid, ids, operation, context=None):
658         """ Access rules of mail.message:
659             - read: if
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
663                 - otherwise: raise
664             - create: if
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
669                 - otherwise: raise
670             - write: if
671                 - author_id == pid, uid is the author, OR
672                 - uid has write or create access on the related document if model, res_id
673                 - otherwise: raise
674             - unlink: if
675                 - uid has write or create access on the related document if model, res_id
676                 - otherwise: raise
677         """
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': ..}}
681             """
682             model_record_ids = {}
683             for id in msg_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
688
689         if uid == SUPERUSER_ID:
690             return
691         if isinstance(ids, (int, long)):
692             ids = [ids]
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
696
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}
702
703         # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
704         author_ids = []
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')]
711
712         # Parent condition, for create (check for received notifications for the created message parent)
713         notified_ids = []
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]
721
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),
729             ], context=context)
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),
737                     ], context=context)
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]
741
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)
751             else:
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]
755
756         # Calculate remaining ids: if not void, raise an error
757         other_ids = other_ids.difference(set(document_related_ids))
758         if not other_ids:
759             return
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))
763
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:
768             return False
769         return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
770
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
774         """
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]
778
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)
784         else:
785             message_id = tools.generate_tracking_message_id('private')
786         return message_id
787
788     def create(self, cr, uid, values, context=None):
789         context = dict(context or {})
790         default_starred = context.pop('default_starred', False)
791
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)
800
801         newid = super(mail_message, self).create(cr, uid, values, context)
802
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.
812         if default_starred:
813             self.set_message_starred(cr, uid, [newid], True, context=context)
814         return newid
815
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)
821         return res
822
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)
835
836     #------------------------------------------------------
837     # Messaging API
838     #------------------------------------------------------
839
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
843         """
844         notification_obj = self.pool.get('mail.notification')
845         message = self.browse(cr, uid, newid, context=context)
846         partners_to_notify = set([])
847
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(
853                 cr, SUPERUSER_ID, [
854                     ('res_model', '=', message.model),
855                     ('res_id', '=', message.res_id),
856                 ], context=context)
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]
860             )
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])
866
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])
870
871         # notify
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
875         )
876         message.refresh()
877
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,
887                         'is_read': True,
888                     }, context=context)