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