[CLEAN] _auto_join -> auto_join.
[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 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'):
80                 continue
81             result[message['id']] = self._shorten_name(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         'author_id': fields.many2one('res.partner', 'Author', select=1,
143             ondelete='set null',
144             help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
145         'partner_ids': fields.many2many('res.partner', string='Recipients'),
146         'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
147             'message_id', 'partner_id', 'Notified partners',
148             help='Partners that have a notification pushing this message in their mailboxes'),
149         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
150             'message_id', 'attachment_id', 'Attachments'),
151         'parent_id': fields.many2one('mail.message', 'Parent Message', select=True,
152             ondelete='set null', help="Initial thread message."),
153         'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
154         'model': fields.char('Related Document Model', size=128, select=1),
155         'res_id': fields.integer('Related Document ID', select=1),
156         'record_name': fields.function(_get_record_name, type='char',
157             store=True, string='Message Record Name',
158             help="Name get of the related document."),
159         'notification_ids': fields.one2many('mail.notification', 'message_id',
160             string='Notifications', auto_join=True,
161             help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
162         'subject': fields.char('Subject'),
163         'date': fields.datetime('Date'),
164         'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
165         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
166         'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
167             type='boolean', string='To read',
168             help='Current user has an unread notification linked to this message'),
169         'starred': fields.function(_get_starred, fnct_search=_search_starred,
170             type='boolean', string='Starred',
171             help='Current user has a starred notification linked to this message'),
172         'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
173             ondelete='set null', select=1,),
174         'vote_user_ids': fields.many2many('res.users', 'mail_vote',
175             'message_id', 'user_id', string='Votes',
176             help='Users that voted for this message'),
177     }
178
179     def _needaction_domain_get(self, cr, uid, context=None):
180         return [('to_read', '=', True)]
181
182     def _get_default_author(self, cr, uid, context=None):
183         return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
184
185     _defaults = {
186         'type': 'email',
187         'date': lambda *a: fields.datetime.now(),
188         'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
189         'body': '',
190     }
191
192     #------------------------------------------------------
193     # Vote/Like
194     #------------------------------------------------------
195
196     def vote_toggle(self, cr, uid, ids, context=None):
197         ''' Toggles vote. Performed using read to avoid access rights issues.
198             Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
199         for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
200             new_has_voted = not (uid in message.get('vote_user_ids'))
201             if new_has_voted:
202                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
203             else:
204                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
205         return new_has_voted or False
206
207     #------------------------------------------------------
208     # Notification API
209     #------------------------------------------------------
210
211     def set_message_read(self, cr, uid, msg_ids, read, context=None):
212         """ Set messages as (un)read. Technically, the notifications related
213             to uid are set to (un)read. If for some msg_ids there are missing
214             notifications (i.e. due to load more or thread parent fetching),
215             they are created.
216
217             :param bool read: set notification as (un)read
218         """
219         notification_obj = self.pool.get('mail.notification')
220         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
221         notif_ids = notification_obj.search(cr, uid, [
222             ('partner_id', '=', user_pid),
223             ('message_id', 'in', msg_ids)
224             ], context=context)
225
226         # all message have notifications: already set them as (un)read
227         if len(notif_ids) == len(msg_ids):
228             return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
229
230         # some messages do not have notifications: find which one, create notification, update read status
231         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
232         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
233         for msg_id in to_create_msg_ids:
234             notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
235         return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
236
237     def set_message_starred(self, cr, uid, msg_ids, starred, context=None):
238         """ Set messages as (un)starred. Technically, the notifications related
239             to uid are set to (un)starred. If for some msg_ids there are missing
240             notifications (i.e. due to load more or thread parent fetching),
241             they are created.
242
243             :param bool starred: set notification as (un)starred
244         """
245         notification_obj = self.pool.get('mail.notification')
246         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
247         notif_ids = notification_obj.search(cr, uid, [
248             ('partner_id', '=', user_pid),
249             ('message_id', 'in', msg_ids)
250             ], context=context)
251
252         # all message have notifications: already set them as (un)starred
253         if len(notif_ids) == len(msg_ids):
254             notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
255             return starred
256
257         # some messages do not have notifications: find which one, create notification, update starred status
258         notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
259         to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
260         for msg_id in to_create_msg_ids:
261             notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context)
262         notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
263         return starred
264
265     #------------------------------------------------------
266     # Message loading for web interface
267     #------------------------------------------------------
268
269     def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
270         """ Post-processing on values given by message_read. This method will
271             handle partners in batch to avoid doing numerous queries.
272
273             :param list messages: list of message, as get_dict result
274             :param dict message_tree: {[msg.id]: msg browse record}
275         """
276         res_partner_obj = self.pool.get('res.partner')
277         ir_attachment_obj = self.pool.get('ir.attachment')
278         pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
279
280         # 1. Aggregate partners (author_id and partner_ids) and attachments
281         partner_ids = set()
282         attachment_ids = set()
283         for key, message in message_tree.iteritems():
284             if message.author_id:
285                 partner_ids |= set([message.author_id.id])
286             if message.partner_ids:
287                 partner_ids |= set([partner.id for partner in message.partner_ids])
288             if message.attachment_ids:
289                 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
290
291         # Filter author_ids uid can see
292         # partner_ids = self.pool.get('res.partner').search(cr, uid, [('id', 'in', partner_ids)], context=context)
293         partners = res_partner_obj.name_get(cr, uid, list(partner_ids), context=context)
294         partner_tree = dict((partner[0], partner) for partner in partners)
295
296         # 2. Attachments
297         attachments = ir_attachment_obj.read(cr, uid, list(attachment_ids), ['id', 'datas_fname'], context=context)
298         attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments)
299
300         # 3. Update message dictionaries
301         for message_dict in messages:
302             message_id = message_dict.get('id')
303             message = message_tree[message_id]
304             if message.author_id:
305                 author = partner_tree[message.author_id.id]
306             else:
307                 author = (0, message.email_from)
308             partner_ids = []
309             for partner in message.partner_ids:
310                 if partner.id in partner_tree:
311                     partner_ids.append(partner_tree[partner.id])
312             attachment_ids = []
313             for attachment in message.attachment_ids:
314                 if attachment.id in attachments_tree:
315                     attachment_ids.append(attachments_tree[attachment.id])
316             message_dict.update({
317                 'is_author': pid == author[0],
318                 'author_id': author,
319                 'partner_ids': partner_ids,
320                 'attachment_ids': attachment_ids,
321                 })
322         return True
323
324     def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
325         """ Return a dict representation of the message. This representation is
326             used in the JS client code, to display the messages. Partners and
327             attachments related stuff will be done in post-processing in batch.
328
329             :param dict message: mail.message browse record
330         """
331         # private message: no model, no res_id
332         is_private = False
333         if not message.model or not message.res_id:
334             is_private = True
335         # votes and favorites: res.users ids, no prefetching should be done
336         vote_nb = len(message.vote_user_ids)
337         has_voted = uid in [user.id for user in message.vote_user_ids]
338
339         return {'id': message.id,
340                 'type': message.type,
341                 'body': html_email_clean(message.body or ''),
342                 'model': message.model,
343                 'res_id': message.res_id,
344                 'record_name': message.record_name,
345                 'subject': message.subject,
346                 'date': message.date,
347                 'to_read': message.to_read,
348                 'parent_id': parent_id,
349                 'is_private': is_private,
350                 'author_id': False,
351                 'is_author': False,
352                 'partner_ids': [],
353                 'vote_nb': vote_nb,
354                 'has_voted': has_voted,
355                 'is_favorite': message.starred,
356                 'attachment_ids': [],
357             }
358
359     def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
360             message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
361         """ Create expandables for message_read, to load new messages.
362             1. get the expandable for new threads
363                 if display is flat (thread_level == 0):
364                     fetch message_ids < min(already displayed ids), because we
365                     want a flat display, ordered by id
366                 else:
367                     fetch message_ids that are not childs of already displayed
368                     messages
369             2. get the expandables for new messages inside threads if display
370                is not flat
371                 for each thread header, search for its childs
372                     for each hole in the child list based on message displayed,
373                     create an expandable
374
375             :param list messages: list of message structure for the Chatter
376                 widget to which expandables are added
377             :param dict message_tree: dict [id]: browse record of this message
378             :param dict parent_tree: dict [parent_id]: [child_ids]
379             :param list message_unload_ids: list of message_ids we do not want
380                 to load
381             :return bool: True
382         """
383         def _get_expandable(domain, message_nb, parent_id, max_limit):
384             return {
385                 'domain': domain,
386                 'nb_messages': message_nb,
387                 'type': 'expandable',
388                 'parent_id': parent_id,
389                 'max_limit':  max_limit,
390             }
391
392         if not messages:
393             return True
394         message_ids = sorted(message_tree.keys())
395
396         # 1. get the expandable for new threads
397         if thread_level == 0:
398             exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
399         else:
400             exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
401         ids = self.search(cr, uid, exp_domain, context=context, limit=1)
402         if ids:
403             # inside a thread: prepend
404             if parent_id:
405                 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
406             # new threads: append
407             else:
408                 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
409
410         # 2. get the expandables for new messages inside threads if display is not flat
411         if thread_level == 0:
412             return True
413         for message_id in message_ids:
414             message = message_tree[message_id]
415
416             # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
417             if message.parent_id:
418                 continue
419
420             # check there are message for expandable
421             child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
422             child_ids = sorted(list(child_ids), reverse=True)
423             if not child_ids:
424                 continue
425
426             # make groups of unread messages
427             id_min, id_max, nb = max(child_ids), 0, 0
428             for child_id in child_ids:
429                 if not child_id in message_ids:
430                     nb += 1
431                     if id_min > child_id:
432                         id_min = child_id
433                     if id_max < child_id:
434                         id_max = child_id
435                 elif nb > 0:
436                     exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
437                     idx = [msg.get('id') for msg in messages].index(child_id) + 1
438                     # messages.append(_get_expandable(exp_domain, nb, message_id, False))
439                     messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
440                     id_min, id_max, nb = max(child_ids), 0, 0
441                 else:
442                     id_min, id_max, nb = max(child_ids), 0, 0
443             if nb > 0:
444                 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
445                 idx = [msg.get('id') for msg in messages].index(message_id) + 1
446                 # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
447                 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
448
449         return True
450
451     def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
452                         thread_level=0, context=None, parent_id=False, limit=None):
453         """ Read messages from mail.message, and get back a list of structured
454             messages to be displayed as discussion threads. If IDs is set,
455             fetch these records. Otherwise use the domain to fetch messages.
456             After having fetch messages, their ancestors will be added to obtain
457             well formed threads, if uid has access to them.
458
459             After reading the messages, expandable messages are added in the
460             message list (see ``_message_read_add_expandables``). It consists
461             in messages holding the 'read more' data: number of messages to
462             read, domain to apply.
463
464             :param list ids: optional IDs to fetch
465             :param list domain: optional domain for searching ids if ids not set
466             :param list message_unload_ids: optional ids we do not want to fetch,
467                 because i.e. they are already displayed somewhere
468             :param int parent_id: context of parent_id
469                 - if parent_id reached when adding ancestors, stop going further
470                   in the ancestor search
471                 - if set in flat mode, ancestor_id is set to parent_id
472             :param int limit: number of messages to fetch, before adding the
473                 ancestors and expandables
474             :return list: list of message structure for the Chatter widget
475         """
476         assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
477         domain = domain if domain is not None else []
478         message_unload_ids = message_unload_ids if message_unload_ids is not None else []
479         if message_unload_ids:
480             domain += [('id', 'not in', message_unload_ids)]
481         limit = limit or self._message_read_limit
482         message_tree = {}
483         message_list = []
484         parent_tree = {}
485
486         # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
487         if ids is None:
488             ids = self.search(cr, uid, domain, context=context, limit=limit)
489
490         # fetch parent if threaded, sort messages
491         for message in self.browse(cr, uid, ids, context=context):
492             message_id = message.id
493             if message_id in message_tree:
494                 continue
495             message_tree[message_id] = message
496
497             # find parent_id
498             if thread_level == 0:
499                 tree_parent_id = parent_id
500             else:
501                 tree_parent_id = message_id
502                 parent = message
503                 while parent.parent_id and parent.parent_id.id != parent_id:
504                     parent = parent.parent_id
505                     tree_parent_id = parent.id
506                 if not parent.id in message_tree:
507                     message_tree[parent.id] = parent
508             # newest messages first
509             parent_tree.setdefault(tree_parent_id, [])
510             if tree_parent_id != message_id:
511                 parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
512
513         if thread_level:
514             for key, message_id_list in parent_tree.iteritems():
515                 message_id_list.sort(key=lambda item: item['id'])
516                 message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
517
518         parent_list = parent_tree.items()
519         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)
520         message_list = [message for (key, msg_list) in parent_list for message in msg_list]
521
522         # get the child expandable messages for the tree
523         self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
524         self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
525             thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
526         return message_list
527
528     #------------------------------------------------------
529     # mail_message internals
530     #------------------------------------------------------
531
532     def init(self, cr):
533         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
534         if not cr.fetchone():
535             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
536
537     def _search(self, cr, uid, args, offset=0, limit=None, order=None,
538         context=None, count=False, access_rights_uid=None):
539         """ Override that adds specific access rights of mail.message, to remove
540             ids uid could not see according to our custom rules. Please refer
541             to check_access_rule for more details about those rules.
542
543             After having received ids of a classic search, keep only:
544             - if author_id == pid, uid is the author, OR
545             - a notification (id, pid) exists, uid has been notified, OR
546             - uid have read access to the related document is model, res_id
547             - otherwise: remove the id
548         """
549         # Rules do not apply to administrator
550         if uid == SUPERUSER_ID:
551             return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
552                 context=context, count=count, access_rights_uid=access_rights_uid)
553         # Perform a super with count as False, to have the ids, not a counter
554         ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
555             context=context, count=False, access_rights_uid=access_rights_uid)
556         if not ids and count:
557             return 0
558         elif not ids:
559             return ids
560
561         pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
562         author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
563         model_ids = {}
564
565         messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
566         for message in messages:
567             if message.get('author_id') and message.get('author_id')[0] == pid:
568                 author_ids.add(message.get('id'))
569             elif pid in message.get('notified_partner_ids'):
570                 partner_ids.add(message.get('id'))
571             elif message.get('model') and message.get('res_id'):
572                 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
573
574         model_access_obj = self.pool.get('ir.model.access')
575         for doc_model, doc_dict in model_ids.iteritems():
576             if not model_access_obj.check(cr, uid, doc_model, 'read', False):
577                 continue
578             doc_ids = doc_dict.keys()
579             allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
580             allowed_ids |= set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
581
582         final_ids = author_ids | partner_ids | allowed_ids
583         if count:
584             return len(final_ids)
585         else:
586             return list(final_ids)
587
588     def check_access_rule(self, cr, uid, ids, operation, context=None):
589         """ Access rules of mail.message:
590             - read: if
591                 - author_id == pid, uid is the author, OR
592                 - mail_notification (id, pid) exists, uid has been notified, OR
593                 - uid have read access to the related document if model, res_id
594                 - otherwise: raise
595             - create: if
596                 - no model, no res_id, I create a private message
597                 - pid in message_follower_ids if model, res_id OR
598                 - uid have write access on the related document if model, res_id, OR
599                 - otherwise: raise
600             - write: if
601                 - uid has write access on the related document if model, res_id
602                 - Otherwise: raise
603             - unlink: if
604                 - uid has write access on the related document if model, res_id
605                 - Otherwise: raise
606         """
607         if uid == SUPERUSER_ID:
608             return
609         if isinstance(ids, (int, long)):
610             ids = [ids]
611         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
612
613         # Read mail_message.ids to have their values
614         message_values = dict.fromkeys(ids)
615         model_record_ids = {}
616         cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
617         for id, rmod, rid, author_id in cr.fetchall():
618             message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
619             if rmod:
620                 model_record_ids.setdefault(rmod, dict()).setdefault(rid, set()).add(id)
621
622         # Author condition, for read and create (private message) -> could become an ir.rule, but not till we do not have a many2one variable field
623         if operation == 'read':
624             author_ids = [mid for mid, message in message_values.iteritems()
625                 if message.get('author_id') and message.get('author_id') == partner_id]
626         elif operation == 'create':
627             author_ids = [mid for mid, message in message_values.iteritems()
628                 if not message.get('model') and not message.get('res_id')]
629         else:
630             author_ids = []
631
632         # 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
633         if operation == 'read':
634             not_obj = self.pool.get('mail.notification')
635             not_ids = not_obj.search(cr, SUPERUSER_ID, [
636                 ('partner_id', '=', partner_id),
637                 ('message_id', 'in', ids),
638             ], context=context)
639             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
640         elif operation == 'create':
641             notified_ids = []
642             for doc_model, doc_dict in model_record_ids.items():
643                 fol_obj = self.pool.get('mail.followers')
644                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
645                     ('res_model', '=', doc_model),
646                     ('res_id', 'in', list(doc_dict.keys())),
647                     ('partner_id', '=', partner_id),
648                     ], context=context)
649                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
650                 notified_ids += [mid for mid, message in message_values.iteritems()
651                     if message.get('res_model') == doc_model and message.get('res_id') in fol_mids]
652         else:
653             notified_ids = []
654
655         # Calculate remaining ids, and related model/res_ids
656         model_record_ids = {}
657         other_ids = set(ids).difference(set(author_ids), set(notified_ids))
658         for id in other_ids:
659             if message_values[id]['res_model']:
660                 model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
661
662         # CRUD: Access rights related to the document
663         document_related_ids = []
664         for model, mids in model_record_ids.items():
665             model_obj = self.pool.get(model)
666             mids = model_obj.exists(cr, uid, mids)
667             if operation in ['create', 'write', 'unlink']:
668                 model_obj.check_access_rights(cr, uid, 'write')
669                 model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
670             else:
671                 model_obj.check_access_rights(cr, uid, operation)
672                 model_obj.check_access_rule(cr, uid, mids, operation, context=context)
673             document_related_ids += [mid for mid, message in message_values.iteritems()
674                 if message.get('res_model') == model and message.get('res_id') in mids]
675
676         # Calculate remaining ids: if not void, raise an error
677         other_ids = other_ids - set(document_related_ids)
678         if not other_ids:
679             return
680         raise orm.except_orm(_('Access Denied'),
681                             _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
682                             (self._description, operation))
683
684     def create(self, cr, uid, values, context=None):
685         if context is None:
686             context = {}
687         default_starred = context.pop('default_starred', False)
688         if not values.get('message_id') and values.get('res_id') and values.get('model'):
689             values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
690         elif not values.get('message_id'):
691             values['message_id'] = tools.generate_tracking_message_id('private')
692         newid = super(mail_message, self).create(cr, uid, values, context)
693         self._notify(cr, SUPERUSER_ID, newid, context=context)
694         # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
695         # Because starred will call set_message_starred, that looks for notifications.
696         # When creating a new mail_message, it will create a notification to a message
697         # that does not exist, leading to an error (key not existing). Also this
698         # this means unread notifications will be created, yet we can not assure
699         # this is what we want.
700         if default_starred:
701             self.set_message_starred(cr, uid, [newid], True, context=context)
702         return newid
703
704     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
705         """ Override to explicitely call check_access_rule, that is not called
706             by the ORM. It instead directly fetches ir.rules and apply them. """
707         self.check_access_rule(cr, uid, ids, 'read', context=context)
708         res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
709         return res
710
711     def unlink(self, cr, uid, ids, context=None):
712         # cascade-delete attachments that are directly attached to the message (should only happen
713         # for mail.messages that act as parent for a standalone mail.mail record).
714         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
715         attachments_to_delete = []
716         for message in self.browse(cr, uid, ids, context=context):
717             for attach in message.attachment_ids:
718                 if attach.res_model == self._name and attach.res_id == message.id:
719                     attachments_to_delete.append(attach.id)
720         if attachments_to_delete:
721             self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
722         return super(mail_message, self).unlink(cr, uid, ids, context=context)
723
724     def copy(self, cr, uid, id, default=None, context=None):
725         """ Overridden to avoid duplicating fields that are unique to each email """
726         if default is None:
727             default = {}
728         default.update(message_id=False, headers=False)
729         return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
730
731     #------------------------------------------------------
732     # Messaging API
733     #------------------------------------------------------
734
735     # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
736     # will be added to email send for notifications. Currently only WIP.
737     MAIL_TEMPLATE = """<div>
738     % if message:
739         ${display_message(message)}
740     % endif
741     % for ctx_msg in context_messages:
742         ${display_message(ctx_msg)}
743     % endfor
744     % if add_expandable:
745         ${display_expandable()}
746     % endif
747     ${display_message(header_message)}
748     </div>
749
750     <%def name="display_message(message)">
751         <div>
752             Subject: ${message.subject}<br />
753             Body: ${message.body}
754         </div>
755     </%def>
756
757     <%def name="display_expandable()">
758         <div>This is an expandable.</div>
759     </%def>
760     """
761
762     def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
763         """
764             1. message.parent_id = False: new thread, no quote_context
765             2. get the lasts messages in the thread before message
766             3. get the message header
767             4. add an expandable between them
768
769             :param dict quote_context: options for quoting
770             :return string: html quote
771         """
772         add_expandable = False
773
774         message = self.browse(cr, uid, id, context=context)
775         if not message.parent_id:
776             return ''
777         context_ids = self.search(cr, uid, [
778             ('parent_id', '=', message.parent_id.id),
779             ('id', '<', message.id),
780             ], limit=limit, context=context)
781
782         if len(context_ids) >= limit:
783             add_expandable = True
784             context_ids = context_ids[0:-1]
785
786         context_ids.append(message.parent_id.id)
787         context_messages = self.browse(cr, uid, context_ids, context=context)
788         header_message = context_messages.pop()
789
790         try:
791             if not add_original:
792                 message = False
793             result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
794                                                         context_messages=context_messages,
795                                                         header_message=header_message,
796                                                         add_expandable=add_expandable,
797                                                         # context kw would clash with mako internals
798                                                         ctx=context,
799                                                         format_exceptions=True)
800             result = result.strip()
801             return result
802         except Exception:
803             _logger.exception("failed to render mako template for quoting message")
804             return ''
805         return result
806
807     def _notify(self, cr, uid, newid, context=None):
808         """ Add the related record followers to the destination partner_ids if is not a private message.
809             Call mail_notification.notify to manage the email sending
810         """
811         message = self.read(cr, uid, newid, ['model', 'res_id', 'author_id', 'subtype_id', 'partner_ids'], context=context)
812
813         partners_to_notify = set([])
814         # message has no subtype_id: pure log message -> no partners, no one notified
815         if not message.get('subtype_id'):
816             return True
817         # all partner_ids of the mail.message have to be notified
818         if message.get('partner_ids'):
819             partners_to_notify |= set(message.get('partner_ids'))
820         # all followers of the mail.message document have to be added as partners and notified
821         if message.get('model') and message.get('res_id'):
822             fol_obj = self.pool.get("mail.followers")
823             fol_ids = fol_obj.search(cr, uid, [
824                 ('res_model', '=', message.get('model')),
825                 ('res_id', '=', message.get('res_id')),
826                 ('subtype_ids', 'in', message.get('subtype_id')[0])
827                 ], context=context)
828             fol_objs = fol_obj.read(cr, uid, fol_ids, ['partner_id'], context=context)
829             partners_to_notify |= set(fol['partner_id'][0] for fol in fol_objs)
830         # remove me from notified partners, unless the message is written on my own wall
831         if message.get('author_id') and message.get('model') == "res.partner" and message.get('res_id') == message.get('author_id')[0]:
832             partners_to_notify |= set([message.get('author_id')[0]])
833         elif message.get('author_id'):
834             partners_to_notify = partners_to_notify - set([message.get('author_id')[0]])
835
836         if partners_to_notify:
837             self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p_id) for p_id in partners_to_notify]}, context=context)
838
839         self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
840
841     #------------------------------------------------------
842     # Tools
843     #------------------------------------------------------
844
845     def check_partners_email(self, cr, uid, partner_ids, context=None):
846         """ Verify that selected partner_ids have an email_address defined.
847             Otherwise throw a warning. """
848         partner_wo_email_lst = []
849         for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
850             if not partner.email:
851                 partner_wo_email_lst.append(partner)
852         if not partner_wo_email_lst:
853             return {}
854         warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
855         for partner in partner_wo_email_lst:
856             warning_msg += '\n- %s' % (partner.name)
857         return {'warning': {
858                     'title': _('Partners email addresses not found'),
859                     'message': warning_msg,
860                     }
861                 }