[FIX] do not show suggestions for anonymous
[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 from openerp import tools
24
25 from email.header import decode_header
26 from openerp import SUPERUSER_ID
27 from openerp.osv import osv, orm, fields
28 from openerp.tools import html_email_clean
29 from openerp.tools.translate import _
30
31 _logger = logging.getLogger(__name__)
32
33 try:
34     from mako.template import Template as MakoTemplate
35 except ImportError:
36     _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
37
38
39 """ Some tools for parsing / creating email fields """
40 def decode(text):
41     """Returns unicode() string conversion of the the given encoded smtp header text"""
42     if text:
43         text = decode_header(text.replace('\r', ''))
44         return ''.join([tools.ustr(x[0], x[1]) for x in text])
45
46
47 class mail_message(osv.Model):
48     """ Messages model: system notification (replacing res.log notifications),
49         comments (OpenChatter discussion) and incoming emails. """
50     _name = 'mail.message'
51     _description = 'Message'
52     _inherit = ['ir.needaction_mixin']
53     _order = 'id desc'
54     _rec_name = 'record_name'
55
56     _message_read_limit = 30
57     _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
58         'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
59     _message_record_name_length = 18
60     _message_read_more_limit = 1024
61
62     def default_get(self, cr, uid, fields, context=None):
63         # protection for `default_type` values leaking from menu action context (e.g. for invoices)
64         if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection:
65             context = dict(context, default_type=None)
66         return super(mail_message, self).default_get(cr, uid, fields, context=context)
67
68     def _shorten_name(self, name):
69         if len(name) <= (self._message_record_name_length + 3):
70             return name
71         return name[:self._message_record_name_length] + '...'
72
73     def _get_record_name(self, cr, uid, ids, name, arg, context=None):
74         """ Return the related document name, using name_get. It is done using
75             SUPERUSER_ID, to be sure to have the record name correctly stored. """
76         # TDE note: regroup by model/ids, to have less queries to perform
77         result = dict.fromkeys(ids, False)
78         for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
79             if not message.get('model') or not message.get('res_id') or not self.pool.get(message['model']):
80                 continue
81             result[message['id']] = self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
82         return result
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.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
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             ('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 read column. """
101         return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.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.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
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 messages to read by the current user. Condition is
119             inversed because we search unread message on a read column. """
120         return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
121
122     def name_get(self, cr, uid, ids, context=None):
123         # name_get may receive int id instead of an id list
124         if isinstance(ids, (int, long)):
125             ids = [ids]
126         res = []
127         for message in self.browse(cr, uid, ids, context=context):
128             name = '%s: %s' % (message.subject or '', message.body or '')
129             res.append((message.id, self._shorten_name(name.lstrip(' :'))))
130         return res
131
132     _columns = {
133         'type': fields.selection([
134                         ('email', 'Email'),
135                         ('comment', 'Comment'),
136                         ('notification', 'System notification'),
137                         ], 'Type',
138             help="Message type: email for email message, notification for system "\
139                  "message, comment for other messages such as user replies"),
140         'email_from': fields.char('From',
141             help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
142         'reply_to': fields.char('Reply-To',
143             help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
144         'author_id': fields.many2one('res.partner', 'Author', select=1,
145             ondelete='set null',
146             help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
147         'partner_ids': fields.many2many('res.partner', string='Recipients'),
148         'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
149             'message_id', 'partner_id', 'Notified partners',
150             help='Partners that have a notification pushing this message in their mailboxes'),
151         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
152             'message_id', 'attachment_id', 'Attachments'),
153         'parent_id': fields.many2one('mail.message', 'Parent Message', select=True,
154             ondelete='set null', help="Initial thread message."),
155         'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
156         'model': fields.char('Related Document Model', size=128, select=1),
157         'res_id': fields.integer('Related Document ID', select=1),
158         'record_name': fields.function(_get_record_name, type='char',
159             store=True, string='Message Record Name',
160             help="Name get of the related document."),
161         'notification_ids': fields.one2many('mail.notification', 'message_id',
162             string='Notifications', auto_join=True,
163             help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
164         'subject': fields.char('Subject'),
165         'date': fields.datetime('Date'),
166         'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
167         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
168         'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
169             type='boolean', string='To read',
170             help='Current user has an unread notification linked to this message'),
171         'starred': fields.function(_get_starred, fnct_search=_search_starred,
172             type='boolean', string='Starred',
173             help='Current user has a starred notification linked to this message'),
174         'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
175             ondelete='set null', select=1,),
176         'vote_user_ids': fields.many2many('res.users', 'mail_vote',
177             'message_id', 'user_id', string='Votes',
178             help='Users that voted for this message'),
179     }
180
181     def _needaction_domain_get(self, cr, uid, context=None):
182         return [('to_read', '=', True)]
183
184     def _get_default_author(self, cr, uid, context=None):
185         return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
186
187     _defaults = {
188         'type': 'email',
189         'date': lambda *a: fields.datetime.now(),
190         'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
191         'body': '',
192         'email_from': lambda self, cr, uid, ctx=None: self.pool.get('mail.mail')._get_default_from(cr, uid, ctx),
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         message = self.browse(cr, uid, id_message, context=context)
217         if attachment_id in [attachment.id for attachment in message.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     def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, context=None):
231         """ Set messages as (un)read. Technically, the notifications related
232             to uid are set to (un)read. If for some msg_ids there are missing
233             notifications (i.e. due to load more or thread parent fetching),
234             they are created.
235
236             :param bool read: set notification as (un)read
237             :param bool create_missing: create notifications for missing entries
238                 (i.e. when acting on displayed messages not notified)
239         """
240         notification_obj = self.pool.get('mail.notification')
241         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
242         domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
243         if not create_missing:
244             domain += [('read', '=', not read)]
245         notif_ids = notification_obj.search(cr, uid, domain, context=context)
246
247         # all message have notifications: already set them as (un)read
248         if len(notif_ids) == len(msg_ids) or not create_missing:
249             return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
250
251         # some messages do not have notifications: find which one, create notification, update read status
252         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
253         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
254         for msg_id in to_create_msg_ids:
255             notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
256         return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
257
258     def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
259         """ Set messages as (un)starred. Technically, the notifications related
260             to uid are set to (un)starred.
261
262             :param bool starred: set notification as (un)starred
263             :param bool create_missing: create notifications for missing entries
264                 (i.e. when acting on displayed messages not notified)
265         """
266         notification_obj = self.pool.get('mail.notification')
267         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
268         domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
269         if not create_missing:
270             domain += [('starred', '=', not starred)]
271         notif_ids = notification_obj.search(cr, uid, domain, context=context)
272
273         # all message have notifications: already set them as (un)starred
274         if len(notif_ids) == len(msg_ids) or not create_missing:
275             notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
276             return starred
277
278         # some messages do not have notifications: find which one, create notification, update starred status
279         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
280         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
281         for msg_id in to_create_msg_ids:
282             notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context)
283         notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
284         return starred
285
286     #------------------------------------------------------
287     # Message loading for web interface
288     #------------------------------------------------------
289
290     def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
291         """ Post-processing on values given by message_read. This method will
292             handle partners in batch to avoid doing numerous queries.
293
294             :param list messages: list of message, as get_dict result
295             :param dict message_tree: {[msg.id]: msg browse record}
296         """
297         res_partner_obj = self.pool.get('res.partner')
298         ir_attachment_obj = self.pool.get('ir.attachment')
299         pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
300
301         # 1. Aggregate partners (author_id and partner_ids) and attachments
302         partner_ids = set()
303         attachment_ids = set()
304         for key, message in message_tree.iteritems():
305             if message.author_id:
306                 partner_ids |= set([message.author_id.id])
307             if message.notified_partner_ids:
308                 partner_ids |= set([partner.id for partner in message.notified_partner_ids])
309             if message.attachment_ids:
310                 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
311         # Read partners as SUPERUSER -> display the names like classic m2o even if no access
312         partners = res_partner_obj.name_get(cr, SUPERUSER_ID, list(partner_ids), context=context)
313         partner_tree = dict((partner[0], partner) for partner in partners)
314
315         # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
316         attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name'], context=context)
317         attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name']}) for attachment in attachments)
318
319         # 3. Update message dictionaries
320         for message_dict in messages:
321             message_id = message_dict.get('id')
322             message = message_tree[message_id]
323             if message.author_id:
324                 author = partner_tree[message.author_id.id]
325             else:
326                 author = (0, message.email_from)
327             partner_ids = []
328             for partner in message.notified_partner_ids:
329                 if partner.id in partner_tree:
330                     partner_ids.append(partner_tree[partner.id])
331             attachment_ids = []
332             for attachment in message.attachment_ids:
333                 if attachment.id in attachments_tree:
334                     attachment_ids.append(attachments_tree[attachment.id])
335             message_dict.update({
336                 'is_author': pid == author[0],
337                 'author_id': author,
338                 'partner_ids': partner_ids,
339                 'attachment_ids': attachment_ids,
340                 })
341         return True
342
343     def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
344         """ Return a dict representation of the message. This representation is
345             used in the JS client code, to display the messages. Partners and
346             attachments related stuff will be done in post-processing in batch.
347
348             :param dict message: mail.message browse record
349         """
350         # private message: no model, no res_id
351         is_private = False
352         if not message.model or not message.res_id:
353             is_private = True
354         # votes and favorites: res.users ids, no prefetching should be done
355         vote_nb = len(message.vote_user_ids)
356         has_voted = uid in [user.id for user in message.vote_user_ids]
357
358         try:
359             body_html = html_email_clean(message.body)
360         except Exception:
361             body_html = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
362             _logger.exception(Exception)
363
364         return {'id': message.id,
365                 'type': message.type,
366                 'body': body_html,
367                 'model': message.model,
368                 'res_id': message.res_id,
369                 'record_name': message.record_name,
370                 'subject': message.subject,
371                 'date': message.date,
372                 'to_read': message.to_read,
373                 'parent_id': parent_id,
374                 'is_private': is_private,
375                 'author_id': False,
376                 'is_author': False,
377                 'partner_ids': [],
378                 'vote_nb': vote_nb,
379                 'has_voted': has_voted,
380                 'is_favorite': message.starred,
381                 'attachment_ids': [],
382             }
383
384     def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
385             message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
386         """ Create expandables for message_read, to load new messages.
387             1. get the expandable for new threads
388                 if display is flat (thread_level == 0):
389                     fetch message_ids < min(already displayed ids), because we
390                     want a flat display, ordered by id
391                 else:
392                     fetch message_ids that are not childs of already displayed
393                     messages
394             2. get the expandables for new messages inside threads if display
395                is not flat
396                 for each thread header, search for its childs
397                     for each hole in the child list based on message displayed,
398                     create an expandable
399
400             :param list messages: list of message structure for the Chatter
401                 widget to which expandables are added
402             :param dict message_tree: dict [id]: browse record of this message
403             :param dict parent_tree: dict [parent_id]: [child_ids]
404             :param list message_unload_ids: list of message_ids we do not want
405                 to load
406             :return bool: True
407         """
408         def _get_expandable(domain, message_nb, parent_id, max_limit):
409             return {
410                 'domain': domain,
411                 'nb_messages': message_nb,
412                 'type': 'expandable',
413                 'parent_id': parent_id,
414                 'max_limit':  max_limit,
415             }
416
417         if not messages:
418             return True
419         message_ids = sorted(message_tree.keys())
420
421         # 1. get the expandable for new threads
422         if thread_level == 0:
423             exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
424         else:
425             exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
426         ids = self.search(cr, uid, exp_domain, context=context, limit=1)
427         if ids:
428             # inside a thread: prepend
429             if parent_id:
430                 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
431             # new threads: append
432             else:
433                 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
434
435         # 2. get the expandables for new messages inside threads if display is not flat
436         if thread_level == 0:
437             return True
438         for message_id in message_ids:
439             message = message_tree[message_id]
440
441             # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
442             if message.parent_id:
443                 continue
444
445             # check there are message for expandable
446             child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
447             child_ids = sorted(list(child_ids), reverse=True)
448             if not child_ids:
449                 continue
450
451             # make groups of unread messages
452             id_min, id_max, nb = max(child_ids), 0, 0
453             for child_id in child_ids:
454                 if not child_id in message_ids:
455                     nb += 1
456                     if id_min > child_id:
457                         id_min = child_id
458                     if id_max < child_id:
459                         id_max = child_id
460                 elif nb > 0:
461                     exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
462                     idx = [msg.get('id') for msg in messages].index(child_id) + 1
463                     # messages.append(_get_expandable(exp_domain, nb, message_id, False))
464                     messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
465                     id_min, id_max, nb = max(child_ids), 0, 0
466                 else:
467                     id_min, id_max, nb = max(child_ids), 0, 0
468             if nb > 0:
469                 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
470                 idx = [msg.get('id') for msg in messages].index(message_id) + 1
471                 # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
472                 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
473
474         return True
475
476     def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
477                         thread_level=0, context=None, parent_id=False, limit=None):
478         """ Read messages from mail.message, and get back a list of structured
479             messages to be displayed as discussion threads. If IDs is set,
480             fetch these records. Otherwise use the domain to fetch messages.
481             After having fetch messages, their ancestors will be added to obtain
482             well formed threads, if uid has access to them.
483
484             After reading the messages, expandable messages are added in the
485             message list (see ``_message_read_add_expandables``). It consists
486             in messages holding the 'read more' data: number of messages to
487             read, domain to apply.
488
489             :param list ids: optional IDs to fetch
490             :param list domain: optional domain for searching ids if ids not set
491             :param list message_unload_ids: optional ids we do not want to fetch,
492                 because i.e. they are already displayed somewhere
493             :param int parent_id: context of parent_id
494                 - if parent_id reached when adding ancestors, stop going further
495                   in the ancestor search
496                 - if set in flat mode, ancestor_id is set to parent_id
497             :param int limit: number of messages to fetch, before adding the
498                 ancestors and expandables
499             :return list: list of message structure for the Chatter widget
500         """
501         assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
502         domain = domain if domain is not None else []
503         message_unload_ids = message_unload_ids if message_unload_ids is not None else []
504         if message_unload_ids:
505             domain += [('id', 'not in', message_unload_ids)]
506         notification_obj = self.pool.get('mail.notification')
507         limit = limit or self._message_read_limit
508         message_tree = {}
509         message_list = []
510         parent_tree = {}
511
512         # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
513         if ids is None:
514             ids = self.search(cr, uid, domain, context=context, limit=limit)
515
516         # fetch parent if threaded, sort messages
517         for message in self.browse(cr, uid, ids, context=context):
518             message_id = message.id
519             if message_id in message_tree:
520                 continue
521             message_tree[message_id] = message
522
523             # find parent_id
524             if thread_level == 0:
525                 tree_parent_id = parent_id
526             else:
527                 tree_parent_id = message_id
528                 parent = message
529                 while parent.parent_id and parent.parent_id.id != parent_id:
530                     parent = parent.parent_id
531                     tree_parent_id = parent.id
532                 if not parent.id in message_tree:
533                     message_tree[parent.id] = parent
534             # newest messages first
535             parent_tree.setdefault(tree_parent_id, [])
536             if tree_parent_id != message_id:
537                 parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
538
539         if thread_level:
540             for key, message_id_list in parent_tree.iteritems():
541                 message_id_list.sort(key=lambda item: item['id'])
542                 message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
543
544         # create final ordered message_list based on parent_tree
545         parent_list = parent_tree.items()
546         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)
547         message_list = [message for (key, msg_list) in parent_list for message in msg_list]
548
549         # get the child expandable messages for the tree
550         self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
551         self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
552             thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
553         return message_list
554
555     #------------------------------------------------------
556     # mail_message internals
557     #------------------------------------------------------
558
559     def init(self, cr):
560         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
561         if not cr.fetchone():
562             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
563
564     def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
565         doc_ids = doc_dict.keys()
566         allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
567         return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
568
569     def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
570         model_access_obj = self.pool.get('ir.model.access')
571         allowed_ids = set()
572         for doc_model, doc_dict in model_ids.iteritems():
573             if not model_access_obj.check(cr, uid, doc_model, 'read', False):
574                 continue
575             allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
576         return allowed_ids
577
578     def _search(self, cr, uid, args, offset=0, limit=None, order=None,
579         context=None, count=False, access_rights_uid=None):
580         """ Override that adds specific access rights of mail.message, to remove
581             ids uid could not see according to our custom rules. Please refer
582             to check_access_rule for more details about those rules.
583
584             After having received ids of a classic search, keep only:
585             - if author_id == pid, uid is the author, OR
586             - a notification (id, pid) exists, uid has been notified, OR
587             - uid have read access to the related document is model, res_id
588             - otherwise: remove the id
589         """
590         # Rules do not apply to administrator
591         if uid == SUPERUSER_ID:
592             return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
593                 context=context, count=count, access_rights_uid=access_rights_uid)
594         # Perform a super with count as False, to have the ids, not a counter
595         ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
596             context=context, count=False, access_rights_uid=access_rights_uid)
597         if not ids and count:
598             return 0
599         elif not ids:
600             return ids
601
602         pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
603         author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
604         model_ids = {}
605
606         messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
607         for message in messages:
608             if message.get('author_id') and message.get('author_id')[0] == pid:
609                 author_ids.add(message.get('id'))
610             elif pid in message.get('notified_partner_ids'):
611                 partner_ids.add(message.get('id'))
612             elif message.get('model') and message.get('res_id'):
613                 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
614
615         allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
616         final_ids = author_ids | partner_ids | allowed_ids
617
618         if count:
619             return len(final_ids)
620         else:
621             # re-construct a list based on ids, because set did not keep the original order
622             id_list = [id for id in ids if id in final_ids]
623             return id_list
624
625     def check_access_rule(self, cr, uid, ids, operation, context=None):
626         """ Access rules of mail.message:
627             - read: if
628                 - author_id == pid, uid is the author, OR
629                 - mail_notification (id, pid) exists, uid has been notified, OR
630                 - uid have read access to the related document if model, res_id
631                 - otherwise: raise
632             - create: if
633                 - no model, no res_id, I create a private message OR
634                 - pid in message_follower_ids if model, res_id OR
635                 - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
636                 - uid have write access on the related document if model, res_id, OR
637                 - otherwise: raise
638             - write: if
639                 - author_id == pid, uid is the author, OR
640                 - uid has write access on the related document if model, res_id
641                 - otherwise: raise
642             - unlink: if
643                 - uid has write access on the related document if model, res_id
644                 - otherwise: raise
645         """
646         def _generate_model_record_ids(msg_val, msg_ids=[]):
647             """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
648                 :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
649             """
650             model_record_ids = {}
651             for id in msg_ids:
652                 if msg_val[id]['model'] and msg_val[id]['res_id']:
653                     model_record_ids.setdefault(msg_val[id]['model'], dict()).setdefault(msg_val[id]['res_id'], set()).add(msg_val[id]['res_id'])
654             return model_record_ids
655
656         if uid == SUPERUSER_ID:
657             return
658         if isinstance(ids, (int, long)):
659             ids = [ids]
660         not_obj = self.pool.get('mail.notification')
661         fol_obj = self.pool.get('mail.followers')
662         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
663
664         # Read mail_message.ids to have their values
665         message_values = dict.fromkeys(ids)
666         cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
667         for id, rmod, rid, author_id, parent_id in cr.fetchall():
668             message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
669
670         # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
671         author_ids = []
672         if operation == 'read' or operation == 'write':
673             author_ids = [mid for mid, message in message_values.iteritems()
674                 if message.get('author_id') and message.get('author_id') == partner_id]
675         elif operation == 'create':
676             author_ids = [mid for mid, message in message_values.iteritems()
677                 if not message.get('model') and not message.get('res_id')]
678
679         # Parent condition, for create (check for received notifications for the created message parent)
680         notified_ids = []
681         if operation == 'create':
682             parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
683                 if message.get('parent_id')]
684             not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
685             not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
686             notified_ids += [mid for mid, message in message_values.iteritems()
687                 if message.get('parent_id') in not_parent_ids]
688
689         # 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
690         other_ids = set(ids).difference(set(author_ids), set(notified_ids))
691         model_record_ids = _generate_model_record_ids(message_values, other_ids)
692         if operation == 'read':
693             not_ids = not_obj.search(cr, SUPERUSER_ID, [
694                 ('partner_id', '=', partner_id),
695                 ('message_id', 'in', ids),
696             ], context=context)
697             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
698         elif operation == 'create':
699             for doc_model, doc_dict in model_record_ids.items():
700                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
701                     ('res_model', '=', doc_model),
702                     ('res_id', 'in', list(doc_dict.keys())),
703                     ('partner_id', '=', partner_id),
704                     ], context=context)
705                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
706                 notified_ids += [mid for mid, message in message_values.iteritems()
707                     if message.get('model') == doc_model and message.get('res_id') in fol_mids]
708
709         # CRUD: Access rights related to the document
710         other_ids = other_ids.difference(set(notified_ids))
711         model_record_ids = _generate_model_record_ids(message_values, other_ids)
712         document_related_ids = []
713         
714         for model, doc_dict in model_record_ids.items():
715             model_obj = self.pool.get(model)
716             mids = model_obj.exists(cr, uid, doc_dict.keys())
717             self.check_related_document(cr, uid, model_obj, mids, operation, context)
718
719             document_related_ids += [mid for mid, message in message_values.iteritems()
720                 if message.get('model') == model and message.get('res_id') in mids]
721
722         # Calculate remaining ids: if not void, raise an error
723         other_ids = other_ids.difference(set(document_related_ids))
724         if not other_ids:
725             return
726         raise orm.except_orm(_('Access Denied'),
727                             _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
728                             (self._description, operation))
729
730     def check_related_document(self, cr, uid, model_obj, mids, operation, context=None):
731         """Concrete check permission rules for related document"""
732         if operation in ['create', 'write', 'unlink']:
733             model_obj.check_access_rights(cr, uid, 'write')
734             model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
735         else:
736             model_obj.check_access_rights(cr, uid, operation)
737             model_obj.check_access_rule(cr, uid, mids, operation, context=context)
738
739     def create(self, cr, uid, values, context=None):
740         if context is None:
741             context = {}
742         default_starred = context.pop('default_starred', False)
743         # generate message_id, to redirect answers to the right discussion thread
744         if not values.get('message_id') and values.get('reply_to'):
745             values['message_id'] = tools.generate_tracking_message_id('reply_to-%(model)s' % values)
746         elif not values.get('message_id') and values.get('res_id') and values.get('model'):
747             values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
748         elif not values.get('message_id'):
749             values['message_id'] = tools.generate_tracking_message_id('private')
750         newid = super(mail_message, self).create(cr, uid, values, context)
751         self._notify(cr, uid, newid, context=context)
752         # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
753         # Because starred will call set_message_starred, that looks for notifications.
754         # When creating a new mail_message, it will create a notification to a message
755         # that does not exist, leading to an error (key not existing). Also this
756         # this means unread notifications will be created, yet we can not assure
757         # this is what we want.
758         if default_starred:
759             self.set_message_starred(cr, uid, [newid], True, context=context)
760         return newid
761
762     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
763         """ Override to explicitely call check_access_rule, that is not called
764             by the ORM. It instead directly fetches ir.rules and apply them. """
765         self.check_access_rule(cr, uid, ids, 'read', context=context)
766         res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
767         return res
768
769     def unlink(self, cr, uid, ids, context=None):
770         # cascade-delete attachments that are directly attached to the message (should only happen
771         # for mail.messages that act as parent for a standalone mail.mail record).
772         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
773         attachments_to_delete = []
774         for message in self.browse(cr, uid, ids, context=context):
775             for attach in message.attachment_ids:
776                 if attach.res_model == self._name and attach.res_id == message.id:
777                     attachments_to_delete.append(attach.id)
778         if attachments_to_delete:
779             self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
780         return super(mail_message, self).unlink(cr, uid, ids, context=context)
781
782     def copy(self, cr, uid, id, default=None, context=None):
783         """ Overridden to avoid duplicating fields that are unique to each email """
784         if default is None:
785             default = {}
786         default.update(message_id=False, headers=False)
787         return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
788
789     #------------------------------------------------------
790     # Messaging API
791     #------------------------------------------------------
792
793     # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
794     # will be added to email send for notifications. Currently only WIP.
795     MAIL_TEMPLATE = """<div>
796     % if message:
797         ${display_message(message)}
798     % endif
799     % for ctx_msg in context_messages:
800         ${display_message(ctx_msg)}
801     % endfor
802     % if add_expandable:
803         ${display_expandable()}
804     % endif
805     ${display_message(header_message)}
806     </div>
807
808     <%def name="display_message(message)">
809         <div>
810             Subject: ${message.subject}<br />
811             Body: ${message.body}
812         </div>
813     </%def>
814
815     <%def name="display_expandable()">
816         <div>This is an expandable.</div>
817     </%def>
818     """
819
820     def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
821         """
822             1. message.parent_id = False: new thread, no quote_context
823             2. get the lasts messages in the thread before message
824             3. get the message header
825             4. add an expandable between them
826
827             :param dict quote_context: options for quoting
828             :return string: html quote
829         """
830         add_expandable = False
831
832         message = self.browse(cr, uid, id, context=context)
833         if not message.parent_id:
834             return ''
835         context_ids = self.search(cr, uid, [
836             ('parent_id', '=', message.parent_id.id),
837             ('id', '<', message.id),
838             ], limit=limit, context=context)
839
840         if len(context_ids) >= limit:
841             add_expandable = True
842             context_ids = context_ids[0:-1]
843
844         context_ids.append(message.parent_id.id)
845         context_messages = self.browse(cr, uid, context_ids, context=context)
846         header_message = context_messages.pop()
847
848         try:
849             if not add_original:
850                 message = False
851             result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
852                                                         context_messages=context_messages,
853                                                         header_message=header_message,
854                                                         add_expandable=add_expandable,
855                                                         # context kw would clash with mako internals
856                                                         ctx=context,
857                                                         format_exceptions=True)
858             result = result.strip()
859             return result
860         except Exception:
861             _logger.exception("failed to render mako template for quoting message")
862             return ''
863         return result
864
865     def _notify(self, cr, uid, newid, context=None):
866         """ Add the related record followers to the destination partner_ids if is not a private message.
867             Call mail_notification.notify to manage the email sending
868         """
869         notification_obj = self.pool.get('mail.notification')
870         message = self.browse(cr, uid, newid, context=context)
871
872         partners_to_notify = set([])
873         # message has no subtype_id: pure log message -> no partners, no one notified
874         if not message.subtype_id:
875             return True
876
877         # all followers of the mail.message document have to be added as partners and notified
878         if message.model and message.res_id:
879             fol_obj = self.pool.get("mail.followers")
880             # browse as SUPERUSER because rules could restrict the search results
881             fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
882                 ('res_model', '=', message.model),
883                 ('res_id', '=', message.res_id),
884                 ('subtype_ids', 'in', message.subtype_id.id)
885                 ], context=context)
886             partners_to_notify |= set(fo.partner_id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
887         # remove me from notified partners, unless the message is written on my own wall
888         if message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
889             partners_to_notify |= set([message.author_id])
890         elif message.author_id:
891             partners_to_notify -= set([message.author_id])
892
893         # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
894         if message.partner_ids:
895             partners_to_notify |= set(message.partner_ids)
896
897         # notify
898         if partners_to_notify:
899             notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context)
900         message.refresh()
901
902         # An error appear when a user receive a notification without notifying
903         # the parent message -> add a read notification for the parent
904         if message.parent_id:
905             # all notified_partner_ids of the mail.message have to be notified for the parented messages
906             partners_to_parent_notify = set(message.notified_partner_ids).difference(message.parent_id.notified_partner_ids)
907             for partner in partners_to_parent_notify:
908                 notification_obj.create(cr, uid, {
909                         'message_id': message.parent_id.id,
910                         'partner_id': partner.id,
911                         'read': True,
912                     }, context=context)
913
914     #------------------------------------------------------
915     # Tools
916     #------------------------------------------------------
917
918     def check_partners_email(self, cr, uid, partner_ids, context=None):
919         """ Verify that selected partner_ids have an email_address defined.
920             Otherwise throw a warning. """
921         partner_wo_email_lst = []
922         for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
923             if not partner.email:
924                 partner_wo_email_lst.append(partner)
925         if not partner_wo_email_lst:
926             return {}
927         warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
928         for partner in partner_wo_email_lst:
929             warning_msg += '\n- %s' % (partner.name)
930         return {'warning': {
931                     'title': _('Partners email addresses not found'),
932                     'message': warning_msg,
933                     }
934                 }