[REF] mail: same_thread field changed into no_auto_thread, its contrary, to avoid...
[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         'no_auto_thread': fields.boolean('No threading for answers',
134             help='Answers do not go in the original document\' discussion thread. This has an impact on the generated message-id.'),
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     }
193
194     #------------------------------------------------------
195     # Vote/Like
196     #------------------------------------------------------
197
198     def vote_toggle(self, cr, uid, ids, context=None):
199         ''' Toggles vote. Performed using read to avoid access rights issues.
200             Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
201         for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
202             new_has_voted = not (uid in message.get('vote_user_ids'))
203             if new_has_voted:
204                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
205             else:
206                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
207         return new_has_voted or False
208
209     #------------------------------------------------------
210     # download an attachment
211     #------------------------------------------------------
212
213     def download_attachment(self, cr, uid, id_message, attachment_id, context=None):
214         """ Return the content of linked attachments. """
215         # this will fail if you cannot read the message
216         message_values = self.read(cr, uid, [id_message], ['attachment_ids'], context=context)[0]
217         if attachment_id in message_values['attachment_ids']:
218             attachment = self.pool.get('ir.attachment').browse(cr, SUPERUSER_ID, attachment_id, context=context)
219             if attachment.datas and attachment.datas_fname:
220                 return {
221                     'base64': attachment.datas,
222                     'filename': attachment.datas_fname,
223                 }
224         return False
225
226     #------------------------------------------------------
227     # Notification API
228     #------------------------------------------------------
229
230     @api.cr_uid_ids_context
231     def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, context=None):
232         """ Set messages as (un)read. Technically, the notifications related
233             to uid are set to (un)read. If for some msg_ids there are missing
234             notifications (i.e. due to load more or thread parent fetching),
235             they are created.
236
237             :param bool read: set notification as (un)read
238             :param bool create_missing: create notifications for missing entries
239                 (i.e. when acting on displayed messages not notified)
240
241             :return number of message mark as read
242         """
243         notification_obj = self.pool.get('mail.notification')
244         user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
245         domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
246         if not create_missing:
247             domain += [('is_read', '=', not read)]
248         notif_ids = notification_obj.search(cr, uid, domain, context=context)
249
250         # all message have notifications: already set them as (un)read
251         if len(notif_ids) == len(msg_ids) or not create_missing:
252             notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
253             return len(notif_ids)
254
255         # some messages do not have notifications: find which one, create notification, update read status
256         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
257         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
258         for msg_id in to_create_msg_ids:
259             notification_obj.create(cr, uid, {'partner_id': user_pid, 'is_read': read, 'message_id': msg_id}, context=context)
260         notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
261         return len(notif_ids)
262
263     @api.cr_uid_ids_context
264     def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
265         """ Set messages as (un)starred. Technically, the notifications related
266             to uid are set to (un)starred.
267
268             :param bool starred: set notification as (un)starred
269             :param bool create_missing: create notifications for missing entries
270                 (i.e. when acting on displayed messages not notified)
271         """
272         notification_obj = self.pool.get('mail.notification')
273         user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
274         domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
275         if not create_missing:
276             domain += [('starred', '=', not starred)]
277         values = {
278             'starred': starred
279         }
280         if starred:
281             values['is_read'] = False
282
283         notif_ids = notification_obj.search(cr, uid, domain, context=context)
284
285         # all message have notifications: already set them as (un)starred
286         if len(notif_ids) == len(msg_ids) or not create_missing:
287             notification_obj.write(cr, uid, notif_ids, values, context=context)
288             return starred
289
290         # some messages do not have notifications: find which one, create notification, update starred status
291         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
292         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
293         for msg_id in to_create_msg_ids:
294             notification_obj.create(cr, uid, dict(values, partner_id=user_pid, message_id=msg_id), context=context)
295         notification_obj.write(cr, uid, notif_ids, values, context=context)
296         return starred
297
298     #------------------------------------------------------
299     # Message loading for web interface
300     #------------------------------------------------------
301
302     def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
303         """ Post-processing on values given by message_read. This method will
304             handle partners in batch to avoid doing numerous queries.
305
306             :param list messages: list of message, as get_dict result
307             :param dict message_tree: {[msg.id]: msg browse record}
308         """
309         res_partner_obj = self.pool.get('res.partner')
310         ir_attachment_obj = self.pool.get('ir.attachment')
311         pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
312
313         # 1. Aggregate partners (author_id and partner_ids) and attachments
314         partner_ids = set()
315         attachment_ids = set()
316         for key, message in message_tree.iteritems():
317             if message.author_id:
318                 partner_ids |= set([message.author_id.id])
319             if message.subtype_id and message.notified_partner_ids:  # take notified people of message with a subtype
320                 partner_ids |= set([partner.id for partner in message.notified_partner_ids])
321             elif not message.subtype_id and message.partner_ids:  # take specified people of message without a subtype (log)
322                 partner_ids |= set([partner.id for partner in message.partner_ids])
323             if message.attachment_ids:
324                 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
325         # Read partners as SUPERUSER -> display the names like classic m2o even if no access
326         partners = res_partner_obj.name_get(cr, SUPERUSER_ID, list(partner_ids), context=context)
327         partner_tree = dict((partner[0], partner) for partner in partners)
328
329         # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
330         attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type_icon'], context=context)
331         attachments_tree = dict((attachment['id'], {
332             'id': attachment['id'],
333             'filename': attachment['datas_fname'],
334             'name': attachment['name'],
335             'file_type_icon': attachment['file_type_icon'],
336         }) for attachment in attachments)
337
338         # 3. Update message dictionaries
339         for message_dict in messages:
340             message_id = message_dict.get('id')
341             message = message_tree[message_id]
342             if message.author_id:
343                 author = partner_tree[message.author_id.id]
344             else:
345                 author = (0, message.email_from)
346             partner_ids = []
347             if message.subtype_id:
348                 partner_ids = [partner_tree[partner.id] for partner in message.notified_partner_ids
349                                 if partner.id in partner_tree]
350             else:
351                 partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
352                                 if partner.id in partner_tree]
353             attachment_ids = []
354             for attachment in message.attachment_ids:
355                 if attachment.id in attachments_tree:
356                     attachment_ids.append(attachments_tree[attachment.id])
357             message_dict.update({
358                 'is_author': pid == author[0],
359                 'author_id': author,
360                 'partner_ids': partner_ids,
361                 'attachment_ids': attachment_ids,
362                 'user_pid': pid
363                 })
364         return True
365
366     def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
367         """ Return a dict representation of the message. This representation is
368             used in the JS client code, to display the messages. Partners and
369             attachments related stuff will be done in post-processing in batch.
370
371             :param dict message: mail.message browse record
372         """
373         # private message: no model, no res_id
374         is_private = False
375         if not message.model or not message.res_id:
376             is_private = True
377         # votes and favorites: res.users ids, no prefetching should be done
378         vote_nb = len(message.vote_user_ids)
379         has_voted = uid in [user.id for user in message.vote_user_ids]
380
381         try:
382             if parent_id:
383                 max_length = 300
384             else:
385                 max_length = 100
386             body_short = html_email_clean(message.body, remove=False, shorten=True, max_length=max_length)
387
388         except Exception:
389             body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
390             _logger.exception(Exception)
391
392         return {'id': message.id,
393                 'type': message.type,
394                 'subtype': message.subtype_id.name if message.subtype_id else False,
395                 'body': message.body,
396                 'body_short': body_short,
397                 'model': message.model,
398                 'res_id': message.res_id,
399                 'record_name': message.record_name,
400                 'subject': message.subject,
401                 'date': message.date,
402                 'to_read': message.to_read,
403                 'parent_id': parent_id,
404                 'is_private': is_private,
405                 'author_id': False,
406                 'author_avatar': message.author_avatar,
407                 'is_author': False,
408                 'partner_ids': [],
409                 'vote_nb': vote_nb,
410                 'has_voted': has_voted,
411                 'is_favorite': message.starred,
412                 'attachment_ids': [],
413             }
414
415     def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
416             message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
417         """ Create expandables for message_read, to load new messages.
418             1. get the expandable for new threads
419                 if display is flat (thread_level == 0):
420                     fetch message_ids < min(already displayed ids), because we
421                     want a flat display, ordered by id
422                 else:
423                     fetch message_ids that are not childs of already displayed
424                     messages
425             2. get the expandables for new messages inside threads if display
426                is not flat
427                 for each thread header, search for its childs
428                     for each hole in the child list based on message displayed,
429                     create an expandable
430
431             :param list messages: list of message structure for the Chatter
432                 widget to which expandables are added
433             :param dict message_tree: dict [id]: browse record of this message
434             :param dict parent_tree: dict [parent_id]: [child_ids]
435             :param list message_unload_ids: list of message_ids we do not want
436                 to load
437             :return bool: True
438         """
439         def _get_expandable(domain, message_nb, parent_id, max_limit):
440             return {
441                 'domain': domain,
442                 'nb_messages': message_nb,
443                 'type': 'expandable',
444                 'parent_id': parent_id,
445                 'max_limit':  max_limit,
446             }
447
448         if not messages:
449             return True
450         message_ids = sorted(message_tree.keys())
451
452         # 1. get the expandable for new threads
453         if thread_level == 0:
454             exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
455         else:
456             exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
457         ids = self.search(cr, uid, exp_domain, context=context, limit=1)
458         if ids:
459             # inside a thread: prepend
460             if parent_id:
461                 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
462             # new threads: append
463             else:
464                 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
465
466         # 2. get the expandables for new messages inside threads if display is not flat
467         if thread_level == 0:
468             return True
469         for message_id in message_ids:
470             message = message_tree[message_id]
471
472             # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
473             if message.parent_id:
474                 continue
475
476             # check there are message for expandable
477             child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
478             child_ids = sorted(list(child_ids), reverse=True)
479             if not child_ids:
480                 continue
481
482             # make groups of unread messages
483             id_min, id_max, nb = max(child_ids), 0, 0
484             for child_id in child_ids:
485                 if not child_id in message_ids:
486                     nb += 1
487                     if id_min > child_id:
488                         id_min = child_id
489                     if id_max < child_id:
490                         id_max = child_id
491                 elif nb > 0:
492                     exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
493                     idx = [msg.get('id') for msg in messages].index(child_id) + 1
494                     # messages.append(_get_expandable(exp_domain, nb, message_id, False))
495                     messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
496                     id_min, id_max, nb = max(child_ids), 0, 0
497                 else:
498                     id_min, id_max, nb = max(child_ids), 0, 0
499             if nb > 0:
500                 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
501                 idx = [msg.get('id') for msg in messages].index(message_id) + 1
502                 # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
503                 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
504
505         return True
506
507     @api.cr_uid_context
508     def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
509                         thread_level=0, context=None, parent_id=False, limit=None):
510         """ Read messages from mail.message, and get back a list of structured
511             messages to be displayed as discussion threads. If IDs is set,
512             fetch these records. Otherwise use the domain to fetch messages.
513             After having fetch messages, their ancestors will be added to obtain
514             well formed threads, if uid has access to them.
515
516             After reading the messages, expandable messages are added in the
517             message list (see ``_message_read_add_expandables``). It consists
518             in messages holding the 'read more' data: number of messages to
519             read, domain to apply.
520
521             :param list ids: optional IDs to fetch
522             :param list domain: optional domain for searching ids if ids not set
523             :param list message_unload_ids: optional ids we do not want to fetch,
524                 because i.e. they are already displayed somewhere
525             :param int parent_id: context of parent_id
526                 - if parent_id reached when adding ancestors, stop going further
527                   in the ancestor search
528                 - if set in flat mode, ancestor_id is set to parent_id
529             :param int limit: number of messages to fetch, before adding the
530                 ancestors and expandables
531             :return list: list of message structure for the Chatter widget
532         """
533         assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
534         domain = domain if domain is not None else []
535         message_unload_ids = message_unload_ids if message_unload_ids is not None else []
536         if message_unload_ids:
537             domain += [('id', 'not in', message_unload_ids)]
538         limit = limit or self._message_read_limit
539         message_tree = {}
540         message_list = []
541         parent_tree = {}
542
543         # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
544         if ids is None:
545             ids = self.search(cr, uid, domain, context=context, limit=limit)
546
547         # fetch parent if threaded, sort messages
548         for message in self.browse(cr, uid, ids, context=context):
549             message_id = message.id
550             if message_id in message_tree:
551                 continue
552             message_tree[message_id] = message
553
554             # find parent_id
555             if thread_level == 0:
556                 tree_parent_id = parent_id
557             else:
558                 tree_parent_id = message_id
559                 parent = message
560                 while parent.parent_id and parent.parent_id.id != parent_id:
561                     parent = parent.parent_id
562                     tree_parent_id = parent.id
563                 if not parent.id in message_tree:
564                     message_tree[parent.id] = parent
565             # newest messages first
566             parent_tree.setdefault(tree_parent_id, [])
567             if tree_parent_id != message_id:
568                 parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
569
570         if thread_level:
571             for key, message_id_list in parent_tree.iteritems():
572                 message_id_list.sort(key=lambda item: item['id'])
573                 message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
574
575         # create final ordered message_list based on parent_tree
576         parent_list = parent_tree.items()
577         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)
578         message_list = [message for (key, msg_list) in parent_list for message in msg_list]
579
580         # get the child expandable messages for the tree
581         self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
582         self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
583             thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
584         return message_list
585
586     #------------------------------------------------------
587     # mail_message internals
588     #------------------------------------------------------
589
590     def init(self, cr):
591         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
592         if not cr.fetchone():
593             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
594
595     def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
596         doc_ids = doc_dict.keys()
597         allowed_doc_ids = self.pool[doc_model].search(cr, uid, [('id', 'in', doc_ids)], context=context)
598         return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
599
600     def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
601         model_access_obj = self.pool.get('ir.model.access')
602         allowed_ids = set()
603         for doc_model, doc_dict in model_ids.iteritems():
604             if not model_access_obj.check(cr, uid, doc_model, 'read', False):
605                 continue
606             allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
607         return allowed_ids
608
609     def _search(self, cr, uid, args, offset=0, limit=None, order=None,
610         context=None, count=False, access_rights_uid=None):
611         """ Override that adds specific access rights of mail.message, to remove
612             ids uid could not see according to our custom rules. Please refer
613             to check_access_rule for more details about those rules.
614
615             After having received ids of a classic search, keep only:
616             - if author_id == pid, uid is the author, OR
617             - a notification (id, pid) exists, uid has been notified, OR
618             - uid have read access to the related document is model, res_id
619             - otherwise: remove the id
620         """
621         # Rules do not apply to administrator
622         if uid == SUPERUSER_ID:
623             return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
624                 context=context, count=count, access_rights_uid=access_rights_uid)
625         # Perform a super with count as False, to have the ids, not a counter
626         ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
627             context=context, count=False, access_rights_uid=access_rights_uid)
628         if not ids and count:
629             return 0
630         elif not ids:
631             return ids
632
633         pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
634         author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
635         model_ids = {}
636
637         messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
638         for message in messages:
639             if message.get('author_id') and message.get('author_id')[0] == pid:
640                 author_ids.add(message.get('id'))
641             elif pid in message.get('notified_partner_ids'):
642                 partner_ids.add(message.get('id'))
643             elif message.get('model') and message.get('res_id'):
644                 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
645
646         allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
647         final_ids = author_ids | partner_ids | allowed_ids
648
649         if count:
650             return len(final_ids)
651         else:
652             # re-construct a list based on ids, because set did not keep the original order
653             id_list = [id for id in ids if id in final_ids]
654             return id_list
655
656     def check_access_rule(self, cr, uid, ids, operation, context=None):
657         """ Access rules of mail.message:
658             - read: if
659                 - author_id == pid, uid is the author, OR
660                 - mail_notification (id, pid) exists, uid has been notified, OR
661                 - uid have read access to the related document if model, res_id
662                 - otherwise: raise
663             - create: if
664                 - no model, no res_id, I create a private message OR
665                 - pid in message_follower_ids if model, res_id OR
666                 - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
667                 - uid have write or create access on the related document if model, res_id, OR
668                 - otherwise: raise
669             - write: if
670                 - author_id == pid, uid is the author, OR
671                 - uid has write or create access on the related document if model, res_id
672                 - otherwise: raise
673             - unlink: if
674                 - uid has write or create access on the related document if model, res_id
675                 - otherwise: raise
676         """
677         def _generate_model_record_ids(msg_val, msg_ids):
678             """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
679                 :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
680             """
681             model_record_ids = {}
682             for id in msg_ids:
683                 vals = msg_val.get(id, {})
684                 if vals.get('model') and vals.get('res_id'):
685                     model_record_ids.setdefault(vals['model'], set()).add(vals['res_id'])
686             return model_record_ids
687
688         if uid == SUPERUSER_ID:
689             return
690         if isinstance(ids, (int, long)):
691             ids = [ids]
692         not_obj = self.pool.get('mail.notification')
693         fol_obj = self.pool.get('mail.followers')
694         partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
695
696         # Read mail_message.ids to have their values
697         message_values = dict.fromkeys(ids, {})
698         cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
699         for id, rmod, rid, author_id, parent_id in cr.fetchall():
700             message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
701
702         # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
703         author_ids = []
704         if operation == 'read' or operation == 'write':
705             author_ids = [mid for mid, message in message_values.iteritems()
706                 if message.get('author_id') and message.get('author_id') == partner_id]
707         elif operation == 'create':
708             author_ids = [mid for mid, message in message_values.iteritems()
709                 if not message.get('model') and not message.get('res_id')]
710
711         # Parent condition, for create (check for received notifications for the created message parent)
712         notified_ids = []
713         if operation == 'create':
714             parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
715                 if message.get('parent_id')]
716             not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
717             not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
718             notified_ids += [mid for mid, message in message_values.iteritems()
719                 if message.get('parent_id') in not_parent_ids]
720
721         # 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
722         other_ids = set(ids).difference(set(author_ids), set(notified_ids))
723         model_record_ids = _generate_model_record_ids(message_values, other_ids)
724         if operation == 'read':
725             not_ids = not_obj.search(cr, SUPERUSER_ID, [
726                 ('partner_id', '=', partner_id),
727                 ('message_id', 'in', ids),
728             ], context=context)
729             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
730         elif operation == 'create':
731             for doc_model, doc_ids in model_record_ids.items():
732                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
733                     ('res_model', '=', doc_model),
734                     ('res_id', 'in', list(doc_ids)),
735                     ('partner_id', '=', partner_id),
736                     ], context=context)
737                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
738                 notified_ids += [mid for mid, message in message_values.iteritems()
739                     if message.get('model') == doc_model and message.get('res_id') in fol_mids]
740
741         # CRUD: Access rights related to the document
742         other_ids = other_ids.difference(set(notified_ids))
743         model_record_ids = _generate_model_record_ids(message_values, other_ids)
744         document_related_ids = []
745         for model, doc_ids in model_record_ids.items():
746             model_obj = self.pool[model]
747             mids = model_obj.exists(cr, uid, list(doc_ids))
748             if hasattr(model_obj, 'check_mail_message_access'):
749                 model_obj.check_mail_message_access(cr, uid, mids, operation, context=context)
750             else:
751                 self.pool['mail.thread'].check_mail_message_access(cr, uid, mids, operation, model_obj=model_obj, context=context)
752             document_related_ids += [mid for mid, message in message_values.iteritems()
753                 if message.get('model') == model and message.get('res_id') in mids]
754
755         # Calculate remaining ids: if not void, raise an error
756         other_ids = other_ids.difference(set(document_related_ids))
757         if not other_ids:
758             return
759         raise orm.except_orm(_('Access Denied'),
760                             _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
761                             (self._description, operation))
762
763     def _get_record_name(self, cr, uid, values, context=None):
764         """ Return the related document name, using name_get. It is done using
765             SUPERUSER_ID, to be sure to have the record name correctly stored. """
766         if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
767             return False
768         return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
769
770     def _get_reply_to(self, cr, uid, values, context=None):
771         """ Return a specific reply_to: alias of the document through message_get_reply_to
772             or take the email_from
773         """
774         model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
775         ctx = dict(context, thread_model=model)
776         return self.pool['mail.thread'].message_get_reply_to(cr, uid, [res_id], default=email_from, context=ctx)[res_id]
777
778     def _get_message_id(self, cr, uid, values, context=None):
779         if values.get('no_auto_thread', False) is True:
780             message_id = tools.generate_tracking_message_id('reply_to')
781         elif values.get('res_id') and values.get('model'):
782             message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
783         else:
784             message_id = tools.generate_tracking_message_id('private')
785         return message_id
786
787     def create(self, cr, uid, values, context=None):
788         context = dict(context or {})
789         default_starred = context.pop('default_starred', False)
790
791         if 'email_from' not in values:  # needed to compute reply_to
792             values['email_from'] = self._get_default_from(cr, uid, context=context)
793         if 'message_id' not in values:
794             values['message_id'] = self._get_message_id(cr, uid, values, context=context)
795         if 'reply_to' not in values:
796             values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
797         if 'record_name' not in values and 'default_record_name' not in context:
798             values['record_name'] = self._get_record_name(cr, uid, values, context=context)
799
800         newid = super(mail_message, self).create(cr, uid, values, context)
801
802         self._notify(cr, uid, newid, context=context,
803                      force_send=context.get('mail_notify_force_send', True),
804                      user_signature=context.get('mail_notify_user_signature', True))
805         # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
806         # Because starred will call set_message_starred, that looks for notifications.
807         # When creating a new mail_message, it will create a notification to a message
808         # that does not exist, leading to an error (key not existing). Also this
809         # this means unread notifications will be created, yet we can not assure
810         # this is what we want.
811         if default_starred:
812             self.set_message_starred(cr, uid, [newid], True, context=context)
813         return newid
814
815     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
816         """ Override to explicitely call check_access_rule, that is not called
817             by the ORM. It instead directly fetches ir.rules and apply them. """
818         self.check_access_rule(cr, uid, ids, 'read', context=context)
819         res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
820         return res
821
822     def unlink(self, cr, uid, ids, context=None):
823         # cascade-delete attachments that are directly attached to the message (should only happen
824         # for mail.messages that act as parent for a standalone mail.mail record).
825         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
826         attachments_to_delete = []
827         for message in self.browse(cr, uid, ids, context=context):
828             for attach in message.attachment_ids:
829                 if attach.res_model == self._name and (attach.res_id == message.id or attach.res_id == 0):
830                     attachments_to_delete.append(attach.id)
831         if attachments_to_delete:
832             self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
833         return super(mail_message, self).unlink(cr, uid, ids, context=context)
834
835     #------------------------------------------------------
836     # Messaging API
837     #------------------------------------------------------
838
839     def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
840         """ Add the related record followers to the destination partner_ids if is not a private message.
841             Call mail_notification.notify to manage the email sending
842         """
843         notification_obj = self.pool.get('mail.notification')
844         message = self.browse(cr, uid, newid, context=context)
845         partners_to_notify = set([])
846
847         # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message)
848         if message.subtype_id and message.model and message.res_id:
849             fol_obj = self.pool.get("mail.followers")
850             # browse as SUPERUSER because rules could restrict the search results
851             fol_ids = fol_obj.search(
852                 cr, SUPERUSER_ID, [
853                     ('res_model', '=', message.model),
854                     ('res_id', '=', message.res_id),
855                 ], context=context)
856             partners_to_notify |= set(
857                 fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
858                 if message.subtype_id.id in [st.id for st in fo.subtype_ids]
859             )
860         # remove me from notified partners, unless the message is written on my own wall
861         if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
862             partners_to_notify |= set([message.author_id.id])
863         elif message.author_id:
864             partners_to_notify -= set([message.author_id.id])
865
866         # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
867         if message.partner_ids:
868             partners_to_notify |= set([p.id for p in message.partner_ids])
869
870         # notify
871         notification_obj._notify(
872             cr, uid, newid, partners_to_notify=list(partners_to_notify), context=context,
873             force_send=force_send, user_signature=user_signature
874         )
875         message.refresh()
876
877         # An error appear when a user receive a notification without notifying
878         # the parent message -> add a read notification for the parent
879         if message.parent_id:
880             # all notified_partner_ids of the mail.message have to be notified for the parented messages
881             partners_to_parent_notify = set(message.notified_partner_ids).difference(message.parent_id.notified_partner_ids)
882             for partner in partners_to_parent_notify:
883                 notification_obj.create(cr, uid, {
884                         'message_id': message.parent_id.id,
885                         'partner_id': partner.id,
886                         'is_read': True,
887                     }, context=context)