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