[IMP] Improved code and passed the data according to the format as suggest (partner_i...
[odoo/odoo.git] / addons / mail / mail_thread.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009-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 base64
23 import datetime
24 import dateutil
25 import email
26 import logging
27 import pytz
28 import time
29 import xmlrpclib
30 from email.message import Message
31
32 from openerp import tools
33 from openerp import SUPERUSER_ID
34 from openerp.addons.mail.mail_message import decode
35 from openerp.osv import fields, osv, orm
36 from openerp.tools.safe_eval import safe_eval as eval
37 from openerp.tools.translate import _
38
39 _logger = logging.getLogger(__name__)
40
41
42 def decode_header(message, header, separator=' '):
43     return separator.join(map(decode, filter(None, message.get_all(header, []))))
44
45
46 class mail_thread(osv.AbstractModel):
47     ''' mail_thread model is meant to be inherited by any model that needs to
48         act as a discussion topic on which messages can be attached. Public
49         methods are prefixed with ``message_`` in order to avoid name
50         collisions with methods of the models that will inherit from this class.
51
52         ``mail.thread`` defines fields used to handle and display the
53         communication history. ``mail.thread`` also manages followers of
54         inheriting classes. All features and expected behavior are managed
55         by mail.thread. Widgets has been designed for the 7.0 and following
56         versions of OpenERP.
57
58         Inheriting classes are not required to implement any method, as the
59         default implementation will work for any model. However it is common
60         to override at least the ``message_new`` and ``message_update``
61         methods (calling ``super``) to add model-specific behavior at
62         creation and update of a thread when processing incoming emails.
63
64         Options:
65             - _mail_flat_thread: if set to True, all messages without parent_id
66                 are automatically attached to the first message posted on the
67                 ressource. If set to False, the display of Chatter is done using
68                 threads, and no parent_id is automatically set.
69     '''
70     _name = 'mail.thread'
71     _description = 'Email Thread'
72     _mail_flat_thread = True
73
74     # Automatic logging system if mail installed
75     # _track = {
76     #   'field': {
77     #       'module.subtype_xml': lambda self, cr, uid, obj, context=None: obj[state] == done,
78     #       'module.subtype_xml2': lambda self, cr, uid, obj, context=None: obj[state] != done,
79     #   },
80     #   'field2': {
81     #       ...
82     #   },
83     # }
84     # where
85     #   :param string field: field name
86     #   :param module.subtype_xml: xml_id of a mail.message.subtype (i.e. mail.mt_comment)
87     #   :param obj: is a browse_record
88     #   :param function lambda: returns whether the tracking should record using this subtype
89     _track = {}
90
91     def get_empty_list_help(self, cr, uid, help, context=None):
92         """ Override of BaseModel.get_empty_list_help() to generate an help message
93             that adds alias information. """
94         model = context.get('empty_list_help_model')
95         res_id = context.get('empty_list_help_id')
96         ir_config_parameter = self.pool.get("ir.config_parameter")
97         catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
98         document_name = context.get('empty_list_help_document_name', _('document'))
99         alias = None
100
101         if catchall_domain and model and res_id:  # specific res_id -> find its alias (i.e. section_id specified)
102             object_id = self.pool.get(model).browse(cr, uid, res_id, context=context)
103             # check that the alias effectively creates new records
104             if object_id.alias_id and object_id.alias_id.alias_name and \
105                     object_id.alias_id.alias_model_id and \
106                     object_id.alias_id.alias_model_id.model == self._name and \
107                     object_id.alias_id.alias_force_thread_id == 0:
108                 alias = object_id.alias_id
109         elif catchall_domain and model:  # no specific res_id given -> generic help message, take an example alias (i.e. alias of some section_id)
110             model_id = self.pool.get('ir.model').search(cr, uid, [("model", "=", self._name)], context=context)[0]
111             alias_obj = self.pool.get('mail.alias')
112             alias_ids = alias_obj.search(cr, uid, [("alias_model_id", "=", model_id), ("alias_name", "!=", False), ('alias_force_thread_id', '=', 0)], context=context, order='id ASC')
113             if alias_ids and len(alias_ids) == 1:  # if several aliases -> incoherent to propose one guessed from nowhere, therefore avoid if several aliases
114                 alias = alias_obj.browse(cr, uid, alias_ids[0], context=context)
115
116         if alias:
117             alias_email = alias.name_get()[0][1]
118             return _("""<p class='oe_view_nocontent_create'>
119                             Click here to add new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a>
120                         </p>
121                         %(static_help)s"""
122                     ) % {
123                         'document': document_name,
124                         'email': alias_email,
125                         'static_help': help or ''
126                     }
127
128         if document_name != 'document' and help and help.find("oe_view_nocontent_create") == -1:
129             return _("<p class='oe_view_nocontent_create'>Click here to add new %(document)s</p>%(static_help)s") % {
130                         'document': document_name,
131                         'static_help': help or '',
132                     }
133
134         return help
135
136     def _get_message_data(self, cr, uid, ids, name, args, context=None):
137         """ Computes:
138             - message_unread: has uid unread message for the document
139             - message_summary: html snippet summarizing the Chatter for kanban views """
140         res = dict((id, dict(message_unread=False, message_unread_count=0, message_summary=' ')) for id in ids)
141         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
142
143         # search for unread messages, directly in SQL to improve performances
144         cr.execute("""  SELECT m.res_id FROM mail_message m
145                         RIGHT JOIN mail_notification n
146                         ON (n.message_id = m.id AND n.partner_id = %s AND (n.read = False or n.read IS NULL))
147                         WHERE m.model = %s AND m.res_id in %s""",
148                     (user_pid, self._name, tuple(ids),))
149         for result in cr.fetchall():
150             res[result[0]]['message_unread'] = True
151             res[result[0]]['message_unread_count'] += 1
152
153         for id in ids:
154             if res[id]['message_unread_count']:
155                 title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
156                 res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New"))
157         return res
158
159     def read_followers_data(self, cr, uid, follower_ids, context=None):
160         datas = []
161         partner_pool = self.pool.get('res.partner')
162         technical_group = self.pool.get('ir.model.data').get_object(cr, uid, 'base', 'group_no_one')
163         for follower in partner_pool.browse(cr, uid, follower_ids, context=context):
164             is_editable = uid in map(lambda x:x.id, technical_group.users)
165             is_uid = uid in map(lambda x:x.id, follower.user_ids)
166             data = (
167                      follower.id,
168                      follower.name,
169                      {'is_editable': is_editable, 'is_uid': is_uid},
170             )
171             datas.append(data)
172         return datas
173
174     def _get_subscription_data(self, cr, uid, ids, name, args, user_pid=None, context=None):
175         """ Computes:
176             - message_subtype_data: data about document subtypes: which are
177                 available, which are followed if any """
178         res = dict((id, dict(message_subtype_data='')) for id in ids)
179         if user_pid is None:
180             user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
181
182         # find current model subtypes, add them to a dictionary
183         subtype_obj = self.pool.get('mail.message.subtype')
184         subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
185         subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False, id=subtype.id)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
186         for id in ids:
187             res[id]['message_subtype_data'] = subtype_dict.copy()
188
189         # find the document followers, update the data
190         fol_obj = self.pool.get('mail.followers')
191         fol_ids = fol_obj.search(cr, uid, [
192             ('partner_id', '=', user_pid),
193             ('res_id', 'in', ids),
194             ('res_model', '=', self._name),
195         ], context=context)
196         for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
197             thread_subtype_dict = res[fol.res_id]['message_subtype_data']
198             for subtype in fol.subtype_ids:
199                 thread_subtype_dict[subtype.name]['followed'] = True
200             res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
201
202         return res
203
204     def _search_message_unread(self, cr, uid, obj=None, name=None, domain=None, context=None):
205         return [('message_ids.to_read', '=', True)]
206
207     def _get_followers(self, cr, uid, ids, name, arg, context=None):
208         fol_obj = self.pool.get('mail.followers')
209         fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids)])
210         res = dict((id, dict(message_follower_ids=[], message_is_follower=False)) for id in ids)
211         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
212         for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids):
213             res[fol.res_id]['message_follower_ids'].append(fol.partner_id.id)
214             if fol.partner_id.id == user_pid:
215                 res[fol.res_id]['message_is_follower'] = True
216         return res
217
218     def _set_followers(self, cr, uid, id, name, value, arg, context=None):
219         if not value:
220             return
221         partner_obj = self.pool.get('res.partner')
222         fol_obj = self.pool.get('mail.followers')
223
224         # read the old set of followers, and determine the new set of followers
225         fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', '=', id)])
226         old = set(fol.partner_id.id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids))
227         new = set(old)
228
229         for command in value or []:
230             if isinstance(command, (int, long)):
231                 new.add(command)
232             elif command[0] == 0:
233                 new.add(partner_obj.create(cr, uid, command[2], context=context))
234             elif command[0] == 1:
235                 partner_obj.write(cr, uid, [command[1]], command[2], context=context)
236                 new.add(command[1])
237             elif command[0] == 2:
238                 partner_obj.unlink(cr, uid, [command[1]], context=context)
239                 new.discard(command[1])
240             elif command[0] == 3:
241                 new.discard(command[1])
242             elif command[0] == 4:
243                 new.add(command[1])
244             elif command[0] == 5:
245                 new.clear()
246             elif command[0] == 6:
247                 new = set(command[2])
248
249         # remove partners that are no longer followers
250         fol_ids = fol_obj.search(cr, SUPERUSER_ID,
251             [('res_model', '=', self._name), ('res_id', '=', id), ('partner_id', 'not in', list(new))])
252         fol_obj.unlink(cr, SUPERUSER_ID, fol_ids)
253
254         # add new followers
255         for partner_id in new - old:
256             fol_obj.create(cr, SUPERUSER_ID, {'res_model': self._name, 'res_id': id, 'partner_id': partner_id})
257
258     def _search_followers(self, cr, uid, obj, name, args, context):
259         """Search function for message_follower_ids
260
261         Do not use with operator 'not in'. Use instead message_is_followers
262         """
263         fol_obj = self.pool.get('mail.followers')
264         res = []
265         for field, operator, value in args:
266             assert field == name
267             # TOFIX make it work with not in
268             assert operator != "not in", "Do not search message_follower_ids with 'not in'"
269             fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('partner_id', operator, value)])
270             res_ids = [fol.res_id for fol in fol_obj.browse(cr, SUPERUSER_ID, fol_ids)]
271             res.append(('id', 'in', res_ids))
272         return res
273
274     def _search_is_follower(self, cr, uid, obj, name, args, context):
275         """Search function for message_is_follower"""
276         res = []
277         for field, operator, value in args:
278             assert field == name
279             partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
280             if (operator == '=' and value) or (operator == '!=' and not value):  # is a follower
281                 res_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
282             else:  # is not a follower or unknown domain
283                 mail_ids = self.search(cr, uid, [('message_follower_ids', 'in', [partner_id])], context=context)
284                 res_ids = self.search(cr, uid, [('id', 'not in', mail_ids)], context=context)
285             res.append(('id', 'in', res_ids))
286         return res
287
288     _columns = {
289         'message_is_follower': fields.function(_get_followers, type='boolean',
290             fnct_search=_search_is_follower, string='Is a Follower', multi='_get_followers,'),
291         'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers,
292             fnct_search=_search_followers, type='many2many',
293             obj='res.partner', string='Followers', multi='_get_followers'),
294         'message_ids': fields.one2many('mail.message', 'res_id',
295             domain=lambda self: [('model', '=', self._name)],
296             auto_join=True,
297             string='Messages',
298             help="Messages and communication history"),
299         'message_unread': fields.function(_get_message_data,
300             fnct_search=_search_message_unread, multi="_get_message_data",
301             type='boolean', string='Unread Messages',
302             help="If checked new messages require your attention."),
303         'message_summary': fields.function(_get_message_data, method=True,
304             type='text', string='Summary', multi="_get_message_data",
305             help="Holds the Chatter summary (number of messages, ...). "\
306                  "This summary is directly in html format in order to "\
307                  "be inserted in kanban views."),
308     }
309
310     #------------------------------------------------------
311     # CRUD overrides for automatic subscription and logging
312     #------------------------------------------------------
313
314     def create(self, cr, uid, values, context=None):
315         """ Chatter override :
316             - subscribe uid
317             - subscribe followers of parent
318             - log a creation message
319         """
320         if context is None:
321             context = {}
322         thread_id = super(mail_thread, self).create(cr, uid, values, context=context)
323
324         # automatic logging unless asked not to (mainly for various testing purpose)
325         if not context.get('mail_create_nolog'):
326             self.message_post(cr, uid, thread_id, body=_('%s created') % (self._description), context=context)
327
328         # subscribe uid unless asked not to
329         if not context.get('mail_create_nosubscribe'):
330             self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
331         # auto_subscribe: take values and defaults into account
332         create_values = set(values.keys())
333         for key, val in context.iteritems():
334             if key.startswith('default_'):
335                 create_values.add(key[8:])
336         self.message_auto_subscribe(cr, uid, [thread_id], list(create_values), context=context)
337
338         # track values
339         tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context)
340         if tracked_fields:
341             initial_values = {thread_id: dict((item, False) for item in tracked_fields)}
342             self.message_track(cr, uid, [thread_id], tracked_fields, initial_values, context=context)
343
344         return thread_id
345
346     def write(self, cr, uid, ids, values, context=None):
347         if isinstance(ids, (int, long)):
348             ids = [ids]
349         # Track initial values of tracked fields
350         tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context)
351         if tracked_fields:
352             records = self.browse(cr, uid, ids, context=context)
353             initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)
354
355         # Perform write, update followers
356         result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
357         self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context)
358
359         # Perform the tracking
360         if tracked_fields:
361             self.message_track(cr, uid, ids, tracked_fields, initial_values, context=context)
362         return result
363
364     def unlink(self, cr, uid, ids, context=None):
365         """ Override unlink to delete messages and followers. This cannot be
366             cascaded, because link is done through (res_model, res_id). """
367         msg_obj = self.pool.get('mail.message')
368         fol_obj = self.pool.get('mail.followers')
369         # delete messages and notifications
370         msg_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
371         msg_obj.unlink(cr, uid, msg_ids, context=context)
372         # delete
373         res = super(mail_thread, self).unlink(cr, uid, ids, context=context)
374         # delete followers
375         fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
376         fol_obj.unlink(cr, SUPERUSER_ID, fol_ids, context=context)
377         return res
378
379     def copy(self, cr, uid, id, default=None, context=None):
380         default = default or {}
381         default['message_ids'] = []
382         default['message_follower_ids'] = []
383         return super(mail_thread, self).copy(cr, uid, id, default=default, context=context)
384
385     #------------------------------------------------------
386     # Automatically log tracked fields
387     #------------------------------------------------------
388
389     def _get_tracked_fields(self, cr, uid, updated_fields, context=None):
390         """ Return a structure of tracked fields for the current model.
391             :param list updated_fields: modified field names
392             :return list: a list of (field_name, column_info obj), containing
393                 always tracked fields and modified on_change fields
394         """
395         lst = []
396         for name, column_info in self._all_columns.items():
397             visibility = getattr(column_info.column, 'track_visibility', False)
398             if visibility == 'always' or (visibility == 'onchange' and name in updated_fields) or name in self._track:
399                 lst.append(name)
400         if not lst:
401             return lst
402         return self.fields_get(cr, uid, lst, context=context)
403
404     def message_track(self, cr, uid, ids, tracked_fields, initial_values, context=None):
405
406         def convert_for_display(value, col_info):
407             if not value and col_info['type'] == 'boolean':
408                 return 'False'
409             if not value:
410                 return ''
411             if col_info['type'] == 'many2one':
412                 return value.name_get()[0][1]
413             if col_info['type'] == 'selection':
414                 return dict(col_info['selection'])[value]
415             return value
416
417         def format_message(message_description, tracked_values):
418             message = ''
419             if message_description:
420                 message = '<span>%s</span>' % message_description
421             for name, change in tracked_values.items():
422                 message += '<div> &nbsp; &nbsp; &bull; <b>%s</b>: ' % change.get('col_info')
423                 if change.get('old_value'):
424                     message += '%s &rarr; ' % change.get('old_value')
425                 message += '%s</div>' % change.get('new_value')
426             return message
427
428         if not tracked_fields:
429             return True
430
431         for browse_record in self.browse(cr, uid, ids, context=context):
432             initial = initial_values[browse_record.id]
433             changes = set()
434             tracked_values = {}
435
436             # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
437             for col_name, col_info in tracked_fields.items():
438                 initial_value = initial[col_name]
439                 record_value = getattr(browse_record, col_name)
440
441                 if record_value == initial_value and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
442                     tracked_values[col_name] = dict(col_info=col_info['string'],
443                                                         new_value=convert_for_display(record_value, col_info))
444                 elif record_value != initial_value and (record_value or initial_value):  # because browse null != False
445                     if getattr(self._all_columns[col_name].column, 'track_visibility', None) in ['always', 'onchange']:
446                         tracked_values[col_name] = dict(col_info=col_info['string'],
447                                                             old_value=convert_for_display(initial_value, col_info),
448                                                             new_value=convert_for_display(record_value, col_info))
449                     if col_name in tracked_fields:
450                         changes.add(col_name)
451             if not changes:
452                 continue
453
454             # find subtypes and post messages or log if no subtype found
455             subtypes = []
456             for field, track_info in self._track.items():
457                 if field not in changes:
458                     continue
459                 for subtype, method in track_info.items():
460                     if method(self, cr, uid, browse_record, context):
461                         subtypes.append(subtype)
462
463             posted = False
464             for subtype in subtypes:
465                 try:
466                     subtype_rec = self.pool.get('ir.model.data').get_object(cr, uid, subtype.split('.')[0], subtype.split('.')[1], context=context)
467                 except ValueError, e:
468                     _logger.debug('subtype %s not found, giving error "%s"' % (subtype, e))
469                     continue
470                 message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
471                 self.message_post(cr, uid, browse_record.id, body=message, subtype=subtype, context=context)
472                 posted = True
473             if not posted:
474                 message = format_message('', tracked_values)
475                 self.message_post(cr, uid, browse_record.id, body=message, context=context)
476         return True
477
478     #------------------------------------------------------
479     # mail.message wrappers and tools
480     #------------------------------------------------------
481
482     def _needaction_domain_get(self, cr, uid, context=None):
483         if self._needaction:
484             return [('message_unread', '=', True)]
485         return []
486
487     def _garbage_collect_attachments(self, cr, uid, context=None):
488         """ Garbage collect lost mail attachments. Those are attachments
489             - linked to res_model 'mail.compose.message', the composer wizard
490             - with res_id 0, because they were created outside of an existing
491                 wizard (typically user input through Chatter or reports
492                 created on-the-fly by the templates)
493             - unused since at least one day (create_date and write_date)
494         """
495         limit_date = datetime.datetime.utcnow() - datetime.timedelta(days=1)
496         limit_date_str = datetime.datetime.strftime(limit_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
497         ir_attachment_obj = self.pool.get('ir.attachment')
498         attach_ids = ir_attachment_obj.search(cr, uid, [
499                             ('res_model', '=', 'mail.compose.message'),
500                             ('res_id', '=', 0),
501                             ('create_date', '<', limit_date_str),
502                             ('write_date', '<', limit_date_str),
503                             ], context=context)
504         ir_attachment_obj.unlink(cr, uid, attach_ids, context=context)
505         return True
506
507     def check_mail_message_access(self, cr, uid, mids, operation, model_obj=None, context=None):
508         """ mail.message check permission rules for related document. This method is
509             meant to be inherited in order to implement addons-specific behavior.
510             A common behavior would be to allow creating messages when having read
511             access rule on the document, for portal document such as issues. """
512         if not model_obj:
513             model_obj = self
514         if operation in ['create', 'write', 'unlink']:
515             model_obj.check_access_rights(cr, uid, 'write')
516             model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
517         else:
518             model_obj.check_access_rights(cr, uid, operation)
519             model_obj.check_access_rule(cr, uid, mids, operation, context=context)
520
521     def _get_formview_action(self, cr, uid, id, model=None, context=None):
522         """ Return an action to open the document. This method is meant to be
523             overridden in addons that want to give specific view ids for example.
524
525             :param int id: id of the document to open
526             :param string model: specific model that overrides self._name
527         """
528         return {
529                 'type': 'ir.actions.act_window',
530                 'res_model': model or self._name,
531                 'view_type': 'form',
532                 'view_mode': 'form',
533                 'views': [(False, 'form')],
534                 'target': 'current',
535                 'res_id': id,
536             }
537
538     def _get_inbox_action_xml_id(self, cr, uid, context=None):
539         """ When redirecting towards the Inbox, choose which action xml_id has
540             to be fetched. This method is meant to be inherited, at least in portal
541             because portal users have a different Inbox action than classic users. """
542         return ('mail', 'action_mail_inbox_feeds')
543
544     def message_redirect_action(self, cr, uid, context=None):
545         """ For a given message, return an action that either
546             - opens the form view of the related document if model, res_id, and
547               read access to the document
548             - opens the Inbox with a default search on the conversation if model,
549               res_id
550             - opens the Inbox with context propagated
551
552         """
553         if context is None:
554             context = {}
555
556         # default action is the Inbox action
557         self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
558         act_model, act_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, *self._get_inbox_action_xml_id(cr, uid, context=context))
559         action = self.pool.get(act_model).read(cr, uid, act_id, [])
560
561         # if msg_id specified: try to redirect to the document or fallback on the Inbox
562         msg_id = context.get('params', {}).get('message_id')
563         if not msg_id:
564             return action
565         msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
566         if msg.model and msg.res_id:
567             action.update({
568                 'context': {
569                     'search_default_model': msg.model,
570                     'search_default_res_id': msg.res_id,
571                 }
572             })
573             if self.pool.get(msg.model).check_access_rights(cr, uid, 'read', raise_exception=False):
574                 try:
575                     model_obj = self.pool.get(msg.model)
576                     model_obj.check_access_rule(cr, uid, [msg.res_id], 'read', context=context)
577                     if not hasattr(model_obj, '_get_formview_action'):
578                         action = self.pool.get('mail.thread')._get_formview_action(cr, uid, msg.res_id, model=msg.model, context=context)
579                     else:
580                         action = model_obj._get_formview_action(cr, uid, msg.res_id, context=context)
581                 except (osv.except_osv, orm.except_orm):
582                     pass
583         return action
584
585     #------------------------------------------------------
586     # Email specific
587     #------------------------------------------------------
588
589     def message_get_reply_to(self, cr, uid, ids, context=None):
590         """ Returns the preferred reply-to email address that is basically
591             the alias of the document, if it exists. """
592         if not self._inherits.get('mail.alias'):
593             return [False for id in ids]
594         return ["%s@%s" % (record['alias_name'], record['alias_domain'])
595                     if record.get('alias_domain') and record.get('alias_name')
596                     else False
597                     for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)]
598
599     #------------------------------------------------------
600     # Mail gateway
601     #------------------------------------------------------
602
603     def message_capable_models(self, cr, uid, context=None):
604         """ Used by the plugin addon, based for plugin_outlook and others. """
605         ret_dict = {}
606         for model_name in self.pool.obj_list():
607             model = self.pool[model_name]
608             if hasattr(model, "message_process") and hasattr(model, "message_post"):
609                 ret_dict[model_name] = model._description
610         return ret_dict
611
612     def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None):
613         """ Find partners related to some header fields of the message.
614
615             :param string message: an email.message instance """
616         s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
617         return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context))
618
619     def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None):
620         """ Verify route validity. Check and rules:
621             1 - if thread_id -> check that document effectively exists; otherwise
622                 fallback on a message_new by resetting thread_id
623             2 - check that message_update exists if thread_id is set; or at least
624                 that message_new exist
625             [ - find author_id if udpate_author is set]
626             3 - if there is an alias, check alias_contact:
627                 'followers' and thread_id:
628                     check on target document that the author is in the followers
629                 'followers' and alias_parent_thread_id:
630                     check on alias parent document that the author is in the
631                     followers
632                 'partners': check that author_id id set
633         """
634
635         assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
636         assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
637
638         message_id = message.get('Message-Id')
639         email_from = decode_header(message, 'From')
640         author_id = message_dict.get('author_id')
641         model, thread_id, alias = route[0], route[1], route[4]
642         model_pool = None
643
644         def _create_bounce_email():
645             mail_mail = self.pool.get('mail.mail')
646             mail_id = mail_mail.create(cr, uid, {
647                             'body_html': '<div><p>Hello,</p>'
648                                 '<p>The following email sent to %s cannot be accepted because this is '
649                                 'a private email address. Only allowed people can contact us at this address.</p></div>'
650                                 '<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')),
651                             'subject': 'Re: %s' % message.get('subject'),
652                             'email_to': message.get('from'),
653                             'auto_delete': True,
654                         }, context=context)
655             mail_mail.send(cr, uid, [mail_id], context=context)
656
657         def _warn(message):
658             _logger.warning('Routing mail with Message-Id %s: route %s: %s',
659                                 message_id, route, message)
660
661         # Wrong model
662         if model and not model in self.pool:
663             if assert_model:
664                 assert model in self.pool, 'Routing: unknown target model %s' % model
665             _warn('unknown target model %s' % model)
666             return ()
667         elif model:
668             model_pool = self.pool[model]
669
670         # Private message: should not contain any thread_id
671         if not model and thread_id:
672             if assert_model:
673                 assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message).'
674             _warn('posting a message without model should be with a null res_id (private message), resetting thread_id')
675             thread_id = 0
676
677         # Existing Document: check if exists; if not, fallback on create if allowed
678         if thread_id and not model_pool.exists(cr, uid, thread_id):
679             if create_fallback:
680                 _warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id))
681                 thread_id = None
682             elif assert_model:
683                 assert model_pool.exists(cr, uid, thread_id), 'Routing: reply to missing document (%s,%s)' % (model, thread_id)
684             else:
685                 _warn('reply to missing document (%s,%s), skipping' % (model, thread_id))
686                 return ()
687
688         # Existing Document: check model accepts the mailgateway
689         if thread_id and not hasattr(model_pool, 'message_update'):
690             if create_fallback:
691                 _warn('model %s does not accept document update, fall back on document creation' % model)
692                 thread_id = None
693             elif assert_model:
694                 assert hasattr(model_pool, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model
695             else:
696                 _warn('model %s does not accept document update, skipping' % model)
697                 return ()
698
699         # New Document: check model accepts the mailgateway
700         if not thread_id and not hasattr(model_pool, 'message_new'):
701             if assert_model:
702                 assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model
703             _warn('model %s does not accept document creation, skipping' % model)
704             return ()
705
706         # Update message author if asked
707         # We do it now because we need it for aliases (contact settings)
708         if not author_id and update_author:
709             author_ids = self._find_partner_from_emails(cr, uid, thread_id, [email_from], model=model, context=context)
710             if author_ids:
711                 author_id = author_ids[0]
712                 message_dict['author_id'] = author_id
713
714         # Alias: check alias_contact settings
715         if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id):
716             if thread_id:
717                 obj = self.pool[model].browse(cr, uid, thread_id, context=context)
718             else:
719                 obj = self.pool[alias.alias_parent_model_id.model].browse(cr, uid, alias.alias_parent_thread_id, context=context)
720             if not author_id or not author_id in [fol.id for fol in obj.message_follower_ids]:
721                 _warn('alias %s restricted to internal followers, skipping' % alias.alias_name)
722                 _create_bounce_email()
723                 return ()
724         elif alias and alias.alias_contact == 'partners' and not author_id:
725             _warn('alias %s does not accept unknown author, skipping' % alias.alias_name)
726             _create_bounce_email()
727             return ()
728
729         return (model, thread_id, route[2], route[3], route[4])
730
731     def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
732                       custom_values=None, context=None):
733         """Attempt to figure out the correct target model, thread_id,
734         custom_values and user_id to use for an incoming message.
735         Multiple values may be returned, if a message had multiple
736         recipients matching existing mail.aliases, for example.
737
738         The following heuristics are used, in this order:
739              1. If the message replies to an existing thread_id, and
740                 properly contains the thread model in the 'In-Reply-To'
741                 header, use this model/thread_id pair, and ignore
742                 custom_value (not needed as no creation will take place)
743              2. Look for a mail.alias entry matching the message
744                 recipient, and use the corresponding model, thread_id,
745                 custom_values and user_id.
746              3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
747                 provided.
748              4. If all the above fails, raise an exception.
749
750            :param string message: an email.message instance
751            :param dict message_dict: dictionary holding message variables
752            :param string model: the fallback model to use if the message
753                does not match any of the currently configured mail aliases
754                (may be None if a matching alias is supposed to be present)
755            :type dict custom_values: optional dictionary of default field values
756                 to pass to ``message_new`` if a new record needs to be created.
757                 Ignored if the thread record already exists, and also if a
758                 matching mail.alias was found (aliases define their own defaults)
759            :param int thread_id: optional ID of the record/thread from ``model``
760                to which this mail should be attached. Only used if the message
761                does not reply to an existing thread and does not match any mail alias.
762            :return: list of [model, thread_id, custom_values, user_id, alias]
763         """
764         assert isinstance(message, Message), 'message must be an email.message.Message at this point'
765         fallback_model = model
766
767         # Get email.message.Message variables for future processing
768         message_id = message.get('Message-Id')
769         email_from = decode_header(message, 'From')
770         email_to = decode_header(message, 'To')
771         references = decode_header(message, 'References')
772         in_reply_to = decode_header(message, 'In-Reply-To')
773
774         # 1. Verify if this is a reply to an existing thread
775         thread_references = references or in_reply_to
776         ref_match = thread_references and tools.reference_re.search(thread_references)
777         if ref_match:
778             thread_id = int(ref_match.group(1))
779             model = ref_match.group(2) or fallback_model
780             if thread_id and model in self.pool:
781                 model_obj = self.pool[model]
782                 if model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
783                     _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
784                                     email_from, email_to, message_id, model, thread_id, custom_values, uid)
785                     route = self.message_route_verify(cr, uid, message, message_dict,
786                                     (model, thread_id, custom_values, uid, None),
787                                     update_author=True, assert_model=True, create_fallback=True, context=context)
788                     return route and [route] or []
789
790         # 2. Reply to a private message
791         if in_reply_to:
792             message_ids = self.pool.get('mail.message').search(cr, uid, [
793                                 ('message_id', '=', in_reply_to),
794                                 '!', ('message_id', 'ilike', 'reply_to')
795                             ], limit=1, context=context)
796             if message_ids:
797                 message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
798                 _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
799                                 email_from, email_to, message_id, message.id, custom_values, uid)
800                 route = self.message_route_verify(cr, uid, message, message_dict,
801                                 (message.model, message.res_id, custom_values, uid, None),
802                                 update_author=True, assert_model=True, create_fallback=True, context=context)
803                 return route and [route] or []
804
805         # 3. Look for a matching mail.alias entry
806         # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
807         # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
808         rcpt_tos = \
809              ','.join([decode_header(message, 'Delivered-To'),
810                        decode_header(message, 'To'),
811                        decode_header(message, 'Cc'),
812                        decode_header(message, 'Resent-To'),
813                        decode_header(message, 'Resent-Cc')])
814         local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)]
815         if local_parts:
816             mail_alias = self.pool.get('mail.alias')
817             alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
818             if alias_ids:
819                 routes = []
820                 for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
821                     user_id = alias.alias_user_id.id
822                     if not user_id:
823                         # TDE note: this could cause crashes, because no clue that the user
824                         # that send the email has the right to create or modify a new document
825                         # Fallback on user_id = uid
826                         # Note: recognized partners will be added as followers anyway
827                         # user_id = self._message_find_user_id(cr, uid, message, context=context)
828                         user_id = uid
829                         _logger.info('No matching user_id for the alias %s', alias.alias_name)
830                     route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)
831                     _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
832                                 email_from, email_to, message_id, route)
833                     route = self.message_route_verify(cr, uid, message, message_dict, route,
834                                 update_author=True, assert_model=True, create_fallback=True, context=context)
835                     if route:
836                         routes.append(route)
837                 return routes
838
839         # 4. Fallback to the provided parameters, if they work
840         if not thread_id:
841             # Legacy: fallback to matching [ID] in the Subject
842             match = tools.res_re.search(decode_header(message, 'Subject'))
843             thread_id = match and match.group(1)
844             # Convert into int (bug spotted in 7.0 because of str)
845             try:
846                 thread_id = int(thread_id)
847             except:
848                 thread_id = False
849         _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
850                     email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
851         route = self.message_route_verify(cr, uid, message, message_dict,
852                         (fallback_model, thread_id, custom_values, uid, None),
853                         update_author=True, assert_model=True, context=context)
854         if route:
855             return [route]
856
857         # AssertionError if no routes found and if no bounce occured
858         assert False, \
859             "No possible route found for incoming message from %s to %s (Message-Id %s:)." \
860             "Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id)
861
862     def message_process(self, cr, uid, model, message, custom_values=None,
863                         save_original=False, strip_attachments=False,
864                         thread_id=None, context=None):
865         """ Process an incoming RFC2822 email message, relying on
866             ``mail.message.parse()`` for the parsing operation,
867             and ``message_route()`` to figure out the target model.
868
869             Once the target model is known, its ``message_new`` method
870             is called with the new message (if the thread record did not exist)
871             or its ``message_update`` method (if it did).
872
873             There is a special case where the target model is False: a reply
874             to a private message. In this case, we skip the message_new /
875             message_update step, to just post a new message using mail_thread
876             message_post.
877
878            :param string model: the fallback model to use if the message
879                does not match any of the currently configured mail aliases
880                (may be None if a matching alias is supposed to be present)
881            :param message: source of the RFC2822 message
882            :type message: string or xmlrpclib.Binary
883            :type dict custom_values: optional dictionary of field values
884                 to pass to ``message_new`` if a new record needs to be created.
885                 Ignored if the thread record already exists, and also if a
886                 matching mail.alias was found (aliases define their own defaults)
887            :param bool save_original: whether to keep a copy of the original
888                 email source attached to the message after it is imported.
889            :param bool strip_attachments: whether to strip all attachments
890                 before processing the message, in order to save some space.
891            :param int thread_id: optional ID of the record/thread from ``model``
892                to which this mail should be attached. When provided, this
893                overrides the automatic detection based on the message
894                headers.
895         """
896         if context is None:
897             context = {}
898
899         # extract message bytes - we are forced to pass the message as binary because
900         # we don't know its encoding until we parse its headers and hence can't
901         # convert it to utf-8 for transport between the mailgate script and here.
902         if isinstance(message, xmlrpclib.Binary):
903             message = str(message.data)
904         # Warning: message_from_string doesn't always work correctly on unicode,
905         # we must use utf-8 strings here :-(
906         if isinstance(message, unicode):
907             message = message.encode('utf-8')
908         msg_txt = email.message_from_string(message)
909
910         # parse the message, verify we are not in a loop by checking message_id is not duplicated
911         msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
912         if strip_attachments:
913             msg.pop('attachments', None)
914         # postpone setting msg.partner_ids after message_post, to avoid double notifications
915         partner_ids = msg.pop('partner_ids', [])
916         if msg.get('message_id'):   # should always be True as message_parse generate one if missing
917             existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [
918                                                                 ('message_id', '=', msg.get('message_id')),
919                                                                 ], context=context)
920             if existing_msg_ids:
921                 _logger.info('Ignored mail from %s to %s with Message-Id %s: found duplicated Message-Id during processing',
922                                 msg.get('from'), msg.get('to'), msg.get('message_id'))
923                 return False
924
925         # find possible routes for the message
926         routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context)
927         thread_id = False
928         for model, thread_id, custom_values, user_id, alias in routes:
929             if self._name == 'mail.thread':
930                 context.update({'thread_model': model})
931             if model:
932                 model_pool = self.pool[model]
933                 assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
934                     "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
935                         (msg['message_id'], model)
936
937                 # disabled subscriptions during message_new/update to avoid having the system user running the
938                 # email gateway become a follower of all inbound messages
939                 nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
940                 if thread_id and hasattr(model_pool, 'message_update'):
941                     model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
942                 else:
943                     thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
944             else:
945                 assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
946                 model_pool = self.pool.get('mail.thread')
947             if not hasattr(model_pool, 'message_post'):
948                 context['thread_model'] = model
949                 model_pool = self.pool['mail.thread']
950             new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
951
952             if partner_ids:
953                 # postponed after message_post, because this is an external message and we don't want to create
954                 # duplicate emails due to notifications
955                 self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
956
957         return thread_id
958
959     def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
960         """Called by ``message_process`` when a new message is received
961            for a given thread model, if the message did not belong to
962            an existing thread.
963            The default behavior is to create a new record of the corresponding
964            model (based on some very basic info extracted from the message).
965            Additional behavior may be implemented by overriding this method.
966
967            :param dict msg_dict: a map containing the email details and
968                                  attachments. See ``message_process`` and
969                                 ``mail.message.parse`` for details.
970            :param dict custom_values: optional dictionary of additional
971                                       field values to pass to create()
972                                       when creating the new thread record.
973                                       Be careful, these values may override
974                                       any other values coming from the message.
975            :param dict context: if a ``thread_model`` value is present
976                                 in the context, its value will be used
977                                 to determine the model of the record
978                                 to create (instead of the current model).
979            :rtype: int
980            :return: the id of the newly created thread object
981         """
982         if context is None:
983             context = {}
984         data = {}
985         if isinstance(custom_values, dict):
986             data = custom_values.copy()
987         model = context.get('thread_model') or self._name
988         model_pool = self.pool[model]
989         fields = model_pool.fields_get(cr, uid, context=context)
990         if 'name' in fields and not data.get('name'):
991             data['name'] = msg_dict.get('subject', '')
992         res_id = model_pool.create(cr, uid, data, context=context)
993         return res_id
994
995     def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
996         """Called by ``message_process`` when a new message is received
997            for an existing thread. The default behavior is to update the record
998            with update_vals taken from the incoming email.
999            Additional behavior may be implemented by overriding this
1000            method.
1001            :param dict msg_dict: a map containing the email details and
1002                                attachments. See ``message_process`` and
1003                                ``mail.message.parse()`` for details.
1004            :param dict update_vals: a dict containing values to update records
1005                               given their ids; if the dict is None or is
1006                               void, no write operation is performed.
1007         """
1008         if update_vals:
1009             self.write(cr, uid, ids, update_vals, context=context)
1010         return True
1011
1012     def _message_extract_payload(self, message, save_original=False):
1013         """Extract body as HTML and attachments from the mail message"""
1014         attachments = []
1015         body = u''
1016         if save_original:
1017             attachments.append(('original_email.eml', message.as_string()))
1018         if not message.is_multipart() or 'text/' in message.get('content-type', ''):
1019             encoding = message.get_content_charset()
1020             body = message.get_payload(decode=True)
1021             body = tools.ustr(body, encoding, errors='replace')
1022             if message.get_content_type() == 'text/plain':
1023                 # text/plain -> <pre/>
1024                 body = tools.append_content_to_html(u'', body, preserve=True)
1025         else:
1026             alternative = (message.get_content_type() == 'multipart/alternative')
1027             for part in message.walk():
1028                 if part.get_content_maintype() == 'multipart':
1029                     continue  # skip container
1030                 filename = part.get_filename()  # None if normal part
1031                 encoding = part.get_content_charset()  # None if attachment
1032                 # 1) Explicit Attachments -> attachments
1033                 if filename or part.get('content-disposition', '').strip().startswith('attachment'):
1034                     attachments.append((filename or 'attachment', part.get_payload(decode=True)))
1035                     continue
1036                 # 2) text/plain -> <pre/>
1037                 if part.get_content_type() == 'text/plain' and (not alternative or not body):
1038                     body = tools.append_content_to_html(body, tools.ustr(part.get_payload(decode=True),
1039                                                                          encoding, errors='replace'), preserve=True)
1040                 # 3) text/html -> raw
1041                 elif part.get_content_type() == 'text/html':
1042                     html = tools.ustr(part.get_payload(decode=True), encoding, errors='replace')
1043                     if alternative:
1044                         body = html
1045                     else:
1046                         body = tools.append_content_to_html(body, html, plaintext=False)
1047                 # 4) Anything else -> attachment
1048                 else:
1049                     attachments.append((filename or 'attachment', part.get_payload(decode=True)))
1050         return body, attachments
1051
1052     def message_parse(self, cr, uid, message, save_original=False, context=None):
1053         """Parses a string or email.message.Message representing an
1054            RFC-2822 email, and returns a generic dict holding the
1055            message details.
1056
1057            :param message: the message to parse
1058            :type message: email.message.Message | string | unicode
1059            :param bool save_original: whether the returned dict
1060                should include an ``original`` attachment containing
1061                the source of the message
1062            :rtype: dict
1063            :return: A dict with the following structure, where each
1064                     field may not be present if missing in original
1065                     message::
1066
1067                     { 'message_id': msg_id,
1068                       'subject': subject,
1069                       'from': from,
1070                       'to': to,
1071                       'cc': cc,
1072                       'body': unified_body,
1073                       'attachments': [('file1', 'bytes'),
1074                                       ('file2', 'bytes')}
1075                     }
1076         """
1077         msg_dict = {
1078             'type': 'email',
1079         }
1080         if not isinstance(message, Message):
1081             if isinstance(message, unicode):
1082                 # Warning: message_from_string doesn't always work correctly on unicode,
1083                 # we must use utf-8 strings here :-(
1084                 message = message.encode('utf-8')
1085             message = email.message_from_string(message)
1086
1087         message_id = message['message-id']
1088         if not message_id:
1089             # Very unusual situation, be we should be fault-tolerant here
1090             message_id = "<%s@localhost>" % time.time()
1091             _logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
1092         msg_dict['message_id'] = message_id
1093
1094         if message.get('Subject'):
1095             msg_dict['subject'] = decode(message.get('Subject'))
1096
1097         # Envelope fields not stored in mail.message but made available for message_new()
1098         msg_dict['from'] = decode(message.get('from'))
1099         msg_dict['to'] = decode(message.get('to'))
1100         msg_dict['cc'] = decode(message.get('cc'))
1101         msg_dict['email_from'] = decode(message.get('from'))
1102         partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
1103         msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
1104
1105         if message.get('Date'):
1106             try:
1107                 date_hdr = decode(message.get('Date'))
1108                 parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
1109                 if parsed_date.utcoffset() is None:
1110                     # naive datetime, so we arbitrarily decide to make it
1111                     # UTC, there's no better choice. Should not happen,
1112                     # as RFC2822 requires timezone offset in Date headers.
1113                     stored_date = parsed_date.replace(tzinfo=pytz.utc)
1114                 else:
1115                     stored_date = parsed_date.astimezone(tz=pytz.utc)
1116             except Exception:
1117                 _logger.warning('Failed to parse Date header %r in incoming mail '
1118                                 'with message-id %r, assuming current date/time.',
1119                                 message.get('Date'), message_id)
1120                 stored_date = datetime.datetime.now()
1121             msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
1122
1123         if message.get('In-Reply-To'):
1124             parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
1125             if parent_ids:
1126                 msg_dict['parent_id'] = parent_ids[0]
1127
1128         if message.get('References') and 'parent_id' not in msg_dict:
1129             parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
1130                                                                          [x.strip() for x in decode(message['References']).split()])])
1131             if parent_ids:
1132                 msg_dict['parent_id'] = parent_ids[0]
1133
1134         msg_dict['body'], msg_dict['attachments'] = self._message_extract_payload(message, save_original=save_original)
1135         return msg_dict
1136
1137     #------------------------------------------------------
1138     # Note specific
1139     #------------------------------------------------------
1140
1141     def log(self, cr, uid, id, message, secondary=False, context=None):
1142         _logger.warning("log() is deprecated. As this module inherit from "\
1143                         "mail.thread, the message will be managed by this "\
1144                         "module instead of by the res.log mechanism. Please "\
1145                         "use mail_thread.message_post() instead of the "\
1146                         "now deprecated res.log.")
1147         self.message_post(cr, uid, [id], message, context=context)
1148
1149     def _message_add_suggested_recipient(self, cr, uid, result, obj, partner=None, email=None, reason='', context=None):
1150         """ Called by message_get_suggested_recipients, to add a suggested
1151             recipient in the result dictionary. The form is :
1152                 partner_id, partner_name<partner_email> or partner_name, reason """
1153         if email and not partner:
1154             # get partner info from email
1155             partner_info = self.message_partner_info_from_emails(cr, uid, obj.id, [email], context=context)[0]
1156             if partner_info.get('partner_id'):
1157                 partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info.get('partner_id')], context=context)[0]
1158         if email and email in [val[1] for val in result[obj.id]]:  # already existing email -> skip
1159             return result
1160         if partner and partner in obj.message_follower_ids:  # recipient already in the followers -> skip
1161             return result
1162         if partner and partner in [val[0] for val in result[obj.id]]:  # already existing partner ID -> skip
1163             return result
1164         if partner and partner.email:  # complete profile: id, name <email>
1165             result[obj.id].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
1166         elif partner:  # incomplete profile: id, name
1167             result[obj.id].append((partner.id, '%s' % (partner.name), reason))
1168         else:  # unknown partner, we are probably managing an email address
1169             result[obj.id].append((False, email, reason))
1170         return result
1171
1172     def message_get_suggested_recipients(self, cr, uid, ids, context=None):
1173         """ Returns suggested recipients for ids. Those are a list of
1174             tuple (partner_id, partner_name, reason), to be managed by Chatter. """
1175         result = dict.fromkeys(ids, list())
1176         if self._all_columns.get('user_id'):
1177             for obj in self.browse(cr, SUPERUSER_ID, ids, context=context):  # SUPERUSER because of a read on res.users that would crash otherwise
1178                 if not obj.user_id or not obj.user_id.partner_id:
1179                     continue
1180                 self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
1181         return result
1182
1183     def _find_partner_from_emails(self, cr, uid, id, emails, model=None, context=None, check_followers=True):
1184         """ Utility method to find partners from email addresses. The rules are :
1185             1 - check in document (model | self, id) followers
1186             2 - try to find a matching partner that is also an user
1187             3 - try to find a matching partner
1188
1189             :param list emails: list of email addresses
1190             :param string model: model to fetch related record; by default self
1191                 is used.
1192             :param boolean check_followers: check in document followers
1193         """
1194         partner_obj = self.pool['res.partner']
1195         partner_ids = []
1196         obj = None
1197         if id and (model or self._name != 'mail.thread') and check_followers:
1198             if model:
1199                 obj = self.pool[model].browse(cr, uid, id, context=context)
1200             else:
1201                 obj = self.browse(cr, uid, id, context=context)
1202         for contact in emails:
1203             partner_id = False
1204             email_address = tools.email_split(contact)
1205             if not email_address:
1206                 partner_ids.append(partner_id)
1207                 continue
1208             email_address = email_address[0]
1209             # first try: check in document's followers
1210             if obj:
1211                 for follower in obj.message_follower_ids:
1212                     if follower.email == email_address:
1213                         partner_id = follower.id
1214             # second try: check in partners that are also users
1215             if not partner_id:
1216                 ids = partner_obj.search(cr, SUPERUSER_ID, [
1217                                                 ('email', 'ilike', email_address),
1218                                                 ('user_ids', '!=', False)
1219                                             ], limit=1, context=context)
1220                 if ids:
1221                     partner_id = ids[0]
1222             # third try: check in partners
1223             if not partner_id:
1224                 ids = partner_obj.search(cr, SUPERUSER_ID, [
1225                                                 ('email', 'ilike', email_address)
1226                                             ], limit=1, context=context)
1227                 if ids:
1228                     partner_id = ids[0]
1229             partner_ids.append(partner_id)
1230         return partner_ids
1231
1232     def message_partner_info_from_emails(self, cr, uid, id, emails, link_mail=False, context=None):
1233         """ Convert a list of emails into a list partner_ids and a list
1234             new_partner_ids. The return value is non conventional because
1235             it is meant to be used by the mail widget.
1236
1237             :return dict: partner_ids and new_partner_ids """
1238         mail_message_obj = self.pool.get('mail.message')
1239         partner_ids = self._find_partner_from_emails(cr, uid, id, emails, context=context)
1240         result = list()
1241         for idx in range(len(emails)):
1242             email_address = emails[idx]
1243             partner_id = partner_ids[idx]
1244             partner_info = {'full_name': email_address, 'partner_id': partner_id}
1245             result.append(partner_info)
1246
1247             # link mail with this from mail to the new partner id
1248             if link_mail and partner_info['partner_id']:
1249                 message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
1250                                     '|',
1251                                     ('email_from', '=', email_address),
1252                                     ('email_from', 'ilike', '<%s>' % email_address),
1253                                     ('author_id', '=', False)
1254                                 ], context=context)
1255                 if message_ids:
1256                     mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': partner_info['partner_id']}, context=context)
1257         return result
1258
1259     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
1260                         subtype=None, parent_id=False, attachments=None, context=None,
1261                         content_subtype='html', **kwargs):
1262         """ Post a new message in an existing thread, returning the new
1263             mail.message ID.
1264
1265             :param int thread_id: thread ID to post into, or list with one ID;
1266                 if False/0, mail.message model will also be set as False
1267             :param str body: body of the message, usually raw HTML that will
1268                 be sanitized
1269             :param str type: see mail_message.type field
1270             :param str content_subtype:: if plaintext: convert body into html
1271             :param int parent_id: handle reply to a previous message by adding the
1272                 parent partners to the message in case of private discussion
1273             :param tuple(str,str) attachments or list id: list of attachment tuples in the form
1274                 ``(name,content)``, where content is NOT base64 encoded
1275
1276             Extra keyword arguments will be used as default column values for the
1277             new mail.message record. Special cases:
1278                 - attachment_ids: supposed not attached to any document; attach them
1279                     to the related document. Should only be set by Chatter.
1280             :return int: ID of newly created mail.message
1281         """
1282         if context is None:
1283             context = {}
1284         if attachments is None:
1285             attachments = {}
1286         mail_message = self.pool.get('mail.message')
1287         ir_attachment = self.pool.get('ir.attachment')
1288
1289         assert (not thread_id) or \
1290                 isinstance(thread_id, (int, long)) or \
1291                 (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), \
1292                 "Invalid thread_id; should be 0, False, an ID or a list with one ID"
1293         if isinstance(thread_id, (list, tuple)):
1294             thread_id = thread_id[0]
1295
1296         # if we're processing a message directly coming from the gateway, the destination model was
1297         # set in the context.
1298         model = False
1299         if thread_id:
1300             model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
1301             if model != self._name and hasattr(self.pool[model], 'message_post'):
1302                 del context['thread_model']
1303                 return self.pool[model].message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs)
1304
1305         #0: Find the message's author, because we need it for private discussion
1306         author_id = kwargs.get('author_id')
1307         if author_id is None:  # keep False values
1308             author_id = self.pool.get('mail.message')._get_default_author(cr, uid, context=context)
1309
1310         # 1: Handle content subtype: if plaintext, converto into HTML
1311         if content_subtype == 'plaintext':
1312             body = tools.plaintext2html(body)
1313
1314         # 2: Private message: add recipients (recipients and author of parent message) - current author
1315         #   + legacy-code management (! we manage only 4 and 6 commands)
1316         partner_ids = set()
1317         kwargs_partner_ids = kwargs.pop('partner_ids', [])
1318         for partner_id in kwargs_partner_ids:
1319             if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2:
1320                 partner_ids.add(partner_id[1])
1321             if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3:
1322                 partner_ids |= set(partner_id[2])
1323             elif isinstance(partner_id, (int, long)):
1324                 partner_ids.add(partner_id)
1325             else:
1326                 pass  # we do not manage anything else
1327         if parent_id and not model:
1328             parent_message = mail_message.browse(cr, uid, parent_id, context=context)
1329             private_followers = set([partner.id for partner in parent_message.partner_ids])
1330             if parent_message.author_id:
1331                 private_followers.add(parent_message.author_id.id)
1332             private_followers -= set([author_id])
1333             partner_ids |= private_followers
1334
1335         # 3. Attachments
1336         #   - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
1337         attachment_ids = kwargs.pop('attachment_ids', []) or []  # because we could receive None (some old code sends None)
1338         if attachment_ids:
1339             filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
1340                 ('res_model', '=', 'mail.compose.message'),
1341                 ('create_uid', '=', uid),
1342                 ('id', 'in', attachment_ids)], context=context)
1343             if filtered_attachment_ids:
1344                 ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
1345         attachment_ids = [(4, id) for id in attachment_ids]
1346         # Handle attachments parameter, that is a dictionary of attachments
1347         for name, content in attachments:
1348             if isinstance(content, unicode):
1349                 content = content.encode('utf-8')
1350             data_attach = {
1351                 'name': name,
1352                 'datas': base64.b64encode(str(content)),
1353                 'datas_fname': name,
1354                 'description': name,
1355                 'res_model': model,
1356                 'res_id': thread_id,
1357             }
1358             attachment_ids.append((0, 0, data_attach))
1359
1360         # 4: mail.message.subtype
1361         subtype_id = False
1362         if subtype:
1363             if '.' not in subtype:
1364                 subtype = 'mail.%s' % subtype
1365             ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
1366             subtype_id = ref and ref[1] or False
1367
1368         # automatically subscribe recipients if asked to
1369         if context.get('mail_post_autofollow') and thread_id and partner_ids:
1370             partner_to_subscribe = partner_ids
1371             if context.get('mail_post_autofollow_partner_ids'):
1372                 partner_to_subscribe = filter(lambda item: item in context.get('mail_post_autofollow_partner_ids'), partner_ids)
1373             self.message_subscribe(cr, uid, [thread_id], list(partner_to_subscribe), context=context)
1374
1375         # _mail_flat_thread: automatically set free messages to the first posted message
1376         if self._mail_flat_thread and not parent_id and thread_id:
1377             message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model)], context=context, order="id ASC", limit=1)
1378             parent_id = message_ids and message_ids[0] or False
1379         # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
1380         elif parent_id:
1381             message_ids = mail_message.search(cr, SUPERUSER_ID, [('id', '=', parent_id), ('parent_id', '!=', False)], context=context)
1382             # avoid loops when finding ancestors
1383             processed_list = []
1384             if message_ids:
1385                 message = mail_message.browse(cr, SUPERUSER_ID, message_ids[0], context=context)
1386                 while (message.parent_id and message.parent_id.id not in processed_list):
1387                     processed_list.append(message.parent_id.id)
1388                     message = message.parent_id
1389                 parent_id = message.id
1390
1391         values = kwargs
1392         values.update({
1393             'author_id': author_id,
1394             'model': model,
1395             'res_id': thread_id or False,
1396             'body': body,
1397             'subject': subject or False,
1398             'type': type,
1399             'parent_id': parent_id,
1400             'attachment_ids': attachment_ids,
1401             'subtype_id': subtype_id,
1402             'partner_ids': [(4, pid) for pid in partner_ids],
1403         })
1404
1405         # Avoid warnings about non-existing fields
1406         for x in ('from', 'to', 'cc'):
1407             values.pop(x, None)
1408
1409         # Create and auto subscribe the author
1410         msg_id = mail_message.create(cr, uid, values, context=context)
1411         message = mail_message.browse(cr, uid, msg_id, context=context)
1412         if message.author_id and thread_id and type != 'notification' and not context.get('mail_create_nosubscribe'):
1413             self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
1414         return msg_id
1415
1416     #------------------------------------------------------
1417     # Followers API
1418     #------------------------------------------------------
1419
1420     def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1421         """ Wrapper to get subtypes data. """
1422         return self._get_subscription_data(cr, uid, ids, None, None, user_pid=user_pid, context=context)
1423
1424     def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
1425         """ Wrapper on message_subscribe, using users. If user_ids is not
1426             provided, subscribe uid instead. """
1427         if user_ids is None:
1428             user_ids = [uid]
1429         partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
1430         return self.message_subscribe(cr, uid, ids, partner_ids, subtype_ids=subtype_ids, context=context)
1431
1432     def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None):
1433         """ Add partners to the records followers. """
1434         mail_followers_obj = self.pool.get('mail.followers')
1435         subtype_obj = self.pool.get('mail.message.subtype')
1436
1437         user_pid = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
1438         if set(partner_ids) == set([user_pid]):
1439             try:
1440                 self.check_access_rights(cr, uid, 'read')
1441             except (osv.except_osv, orm.except_orm):
1442                 return
1443         else:
1444             self.check_access_rights(cr, uid, 'write')
1445
1446         for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
1447             existing_pids = set([f.id for f in record.message_follower_ids
1448                                             if f.id in partner_ids])
1449             new_pids = set(partner_ids) - existing_pids
1450
1451             # subtype_ids specified: update already subscribed partners
1452             if subtype_ids and existing_pids:
1453                 fol_ids = mail_followers_obj.search(cr, SUPERUSER_ID, [
1454                                                         ('res_model', '=', self._name),
1455                                                         ('res_id', '=', record.id),
1456                                                         ('partner_id', 'in', list(existing_pids)),
1457                                                     ], context=context)
1458                 mail_followers_obj.write(cr, SUPERUSER_ID, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
1459             # subtype_ids not specified: do not update already subscribed partner, fetch default subtypes for new partners
1460             elif subtype_ids is None:
1461                 subtype_ids = subtype_obj.search(cr, uid, [
1462                                                         ('default', '=', True),
1463                                                         '|',
1464                                                         ('res_model', '=', self._name),
1465                                                         ('res_model', '=', False)
1466                                                     ], context=context)
1467             # subscribe new followers
1468             for new_pid in new_pids:
1469                 mail_followers_obj.create(cr, SUPERUSER_ID, {
1470                                                 'res_model': self._name,
1471                                                 'res_id': record.id,
1472                                                 'partner_id': new_pid,
1473                                                 'subtype_ids': [(6, 0, subtype_ids)],
1474                                             }, context=context)
1475
1476         return True
1477
1478     def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None):
1479         """ Wrapper on message_subscribe, using users. If user_ids is not
1480             provided, unsubscribe uid instead. """
1481         if user_ids is None:
1482             user_ids = [uid]
1483         partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
1484         return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context)
1485
1486     def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
1487         """ Remove partners from the records followers. """
1488         user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
1489         if set(partner_ids) == set([user_pid]):
1490             self.check_access_rights(cr, uid, 'read')
1491         else:
1492             self.check_access_rights(cr, uid, 'write')
1493         return self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
1494
1495     def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=['user_id'], context=None):
1496         """ Returns the list of relational fields linking to res.users that should
1497             trigger an auto subscribe. The default list checks for the fields
1498             - called 'user_id'
1499             - linking to res.users
1500             - with track_visibility set
1501             In OpenERP V7, this is sufficent for all major addon such as opportunity,
1502             project, issue, recruitment, sale.
1503             Override this method if a custom behavior is needed about fields
1504             that automatically subscribe users.
1505         """
1506         user_field_lst = []
1507         for name, column_info in self._all_columns.items():
1508             if name in auto_follow_fields and name in updated_fields and getattr(column_info.column, 'track_visibility', False) and column_info.column._obj == 'res.users':
1509                 user_field_lst.append(name)
1510         return user_field_lst
1511
1512     def message_auto_subscribe(self, cr, uid, ids, updated_fields, context=None):
1513         """
1514             1. fetch project subtype related to task (parent_id.res_model = 'project.task')
1515             2. for each project subtype: subscribe the follower to the task
1516         """
1517         subtype_obj = self.pool.get('mail.message.subtype')
1518         follower_obj = self.pool.get('mail.followers')
1519
1520         # fetch auto_follow_fields
1521         user_field_lst = self._message_get_auto_subscribe_fields(cr, uid, updated_fields, context=context)
1522
1523         # fetch related record subtypes
1524         related_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('parent_id.res_model', '=', self._name)], context=context)
1525         subtypes = subtype_obj.browse(cr, uid, related_subtype_ids, context=context)
1526         default_subtypes = [subtype for subtype in subtypes if subtype.res_model == False]
1527         related_subtypes = [subtype for subtype in subtypes if subtype.res_model != False]
1528         relation_fields = set([subtype.relation_field for subtype in subtypes if subtype.relation_field != False])
1529         if (not related_subtypes or not any(relation in updated_fields for relation in relation_fields)) and not user_field_lst:
1530             return True
1531
1532         for record in self.browse(cr, uid, ids, context=context):
1533             new_followers = dict()
1534             parent_res_id = False
1535             parent_model = False
1536             for subtype in related_subtypes:
1537                 if not subtype.relation_field or not subtype.parent_id:
1538                     continue
1539                 if not subtype.relation_field in self._columns or not getattr(record, subtype.relation_field, False):
1540                     continue
1541                 parent_res_id = getattr(record, subtype.relation_field).id
1542                 parent_model = subtype.res_model
1543                 follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
1544                     ('res_model', '=', parent_model),
1545                     ('res_id', '=', parent_res_id),
1546                     ('subtype_ids', 'in', [subtype.id])
1547                     ], context=context)
1548                 for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
1549                     new_followers.setdefault(follower.partner_id.id, set()).add(subtype.parent_id.id)
1550
1551             if parent_res_id and parent_model:
1552                 for subtype in default_subtypes:
1553                     follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
1554                         ('res_model', '=', parent_model),
1555                         ('res_id', '=', parent_res_id),
1556                         ('subtype_ids', 'in', [subtype.id])
1557                         ], context=context)
1558                     for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
1559                         new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id)
1560
1561             # add followers coming from res.users relational fields that are tracked
1562             user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
1563             user_id_partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]
1564             for partner_id in user_id_partner_ids:
1565                 new_followers.setdefault(partner_id, None)
1566
1567             for pid, subtypes in new_followers.items():
1568                 subtypes = list(subtypes) if subtypes is not None else None
1569                 self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
1570
1571             # find first email message, set it as unread for auto_subscribe fields for them to have a notification
1572             if user_id_partner_ids:
1573                 msg_ids = self.pool.get('mail.message').search(cr, uid, [
1574                                 ('model', '=', self._name),
1575                                 ('res_id', '=', record.id),
1576                                 ('type', '=', 'email')], limit=1, context=context)
1577                 if not msg_ids and record.message_ids:
1578                     msg_ids = [record.message_ids[-1].id]
1579                 if msg_ids:
1580                     self.pool.get('mail.notification')._notify(cr, uid, msg_ids[0], partners_to_notify=user_id_partner_ids, context=context)
1581
1582         return True
1583
1584     #------------------------------------------------------
1585     # Thread state
1586     #------------------------------------------------------
1587
1588     def message_mark_as_unread(self, cr, uid, ids, context=None):
1589         """ Set as unread. """
1590         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
1591         cr.execute('''
1592             UPDATE mail_notification SET
1593                 read=false
1594             WHERE
1595                 message_id IN (SELECT id from mail_message where res_id=any(%s) and model=%s limit 1) and
1596                 partner_id = %s
1597         ''', (ids, self._name, partner_id))
1598         return True
1599
1600     def message_mark_as_read(self, cr, uid, ids, context=None):
1601         """ Set as read. """
1602         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
1603         cr.execute('''
1604             UPDATE mail_notification SET
1605                 read=true
1606             WHERE
1607                 message_id IN (SELECT id FROM mail_message WHERE res_id=ANY(%s) AND model=%s) AND
1608                 partner_id = %s
1609         ''', (ids, self._name, partner_id))
1610         return True
1611
1612     #------------------------------------------------------
1613     # Thread suggestion
1614     #------------------------------------------------------
1615
1616     def get_suggested_thread(self, cr, uid, removed_suggested_threads=None, context=None):
1617         """Return a list of suggested threads, sorted by the numbers of followers"""
1618         if context is None:
1619             context = {}
1620
1621         # TDE HACK: originally by MAT from portal/mail_mail.py but not working until the inheritance graph bug is not solved in trunk
1622         # TDE FIXME: relocate in portal when it won't be necessary to reload the hr.employee model in an additional bridge module
1623         if self.pool['res.groups']._all_columns.get('is_portal'):
1624             user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
1625             if any(group.is_portal for group in user.groups_id):
1626                 return []
1627
1628         threads = []
1629         if removed_suggested_threads is None:
1630             removed_suggested_threads = []
1631
1632         thread_ids = self.search(cr, uid, [('id', 'not in', removed_suggested_threads), ('message_is_follower', '=', False)], context=context)
1633         for thread in self.browse(cr, uid, thread_ids, context=context):
1634             data = {
1635                 'id': thread.id,
1636                 'popularity': len(thread.message_follower_ids),
1637                 'name': thread.name,
1638                 'image_small': thread.image_small
1639             }
1640             threads.append(data)
1641         return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]