[MERGE] latest trunk
[odoo/odoo.git] / addons / mail / mail_thread.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
19 #
20 ##############################################################################
21
22 import base64
23 import email
24 import logging
25 import re
26 import time
27 import xmlrpclib
28 from email.utils import parsedate
29 from email.message import Message
30
31 from osv import osv, fields
32 from mail_message import decode, to_email
33 import tools
34 from tools.translate import _
35 from tools.safe_eval import safe_eval as eval
36
37 _logger = logging.getLogger(__name__)
38
39 def decode_header(message, header, separator=' '):
40     return separator.join(map(decode,message.get_all(header, [])))
41
42 class many2many_reference(fields.many2many):
43     """ many2many_reference is an override of fields.many2many. It manages
44         many2many-like table where one id is given by two fields, res_model
45         and res_id.
46     """
47     
48     def _get_query_and_where_params(self, cr, model, ids, values, where_params):
49         """ Add in where:
50             - mail_followers.res_model = 'crm.lead'
51         """
52         query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
53                     FROM %(rel)s, %(from_c)s \
54                     WHERE %(rel)s.%(id1)s IN %%s \
55                     AND %(rel)s.%(id2)s = %(tbl)s.id \
56                     AND %(rel)s.res_model = %%s \
57                     %(where_c)s  \
58                     %(order_by)s \
59                     %(limit)s \
60                     OFFSET %(offset)d' \
61                 % values
62         where_params = [model._name] + where_params
63         return query, where_params
64
65     def set(self, cr, model, id, name, values, user=None, context=None):
66         """ Override to add the res_model field in queries. """
67         if not values: return
68         rel, id1, id2 = self._sql_names(model)
69         obj = model.pool.get(self._obj)
70         for act in values:
71             if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
72                 continue
73             if act[0] == 0:
74                 idnew = obj.create(cr, user, act[2], context=context)
75                 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, idnew, model._name))
76             elif act[0] == 3:
77                 cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND res_model=%s', (id, act[1], model._name))
78             elif act[0] == 4:
79                 # following queries are in the same transaction - so should be relatively safe
80                 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND res_model=%s', (id, act[1], model._name))
81                 if not cr.fetchone():
82                     cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, act[1], model._name))
83             elif act[0] == 6:
84                 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
85                 if d1:
86                     d1 = ' and ' + ' and '.join(d1)
87                 else:
88                     d1 = ''
89                 cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND res_model=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2)
90                 for act_nbr in act[2]:
91                     cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, act_nbr, model._name))
92             else:
93                 return super(many2many_reference, self).set(cr, model, id, name, values, user, context)
94
95 class mail_thread(osv.Model):
96     '''Mixin model, meant to be inherited by any model that needs to
97        act as a discussion topic on which messages can be attached.
98        Public methods are prefixed with ``message_`` in order to avoid
99        name collisions with methods of the models that will inherit
100        from this mixin.
101
102        ``mail.thread`` is designed to work without adding any field
103        to the extended models. All functionalities and expected behavior
104        are managed by mail.thread, using model name and record ids.
105        A widget has been designed for the 6.1 and following version of OpenERP
106        web-client. However, due to technical limitations, ``mail.thread``
107        adds a simulated one2many field, to display the web widget by
108        overriding the default field displayed. Using this field
109        is not recommanded has it will disappeear in future version
110        of OpenERP, leading to a pure mixin class.
111
112        Inheriting classes are not required to implement any method, as the
113        default implementation will work for any model. However it is common
114        to override at least the ``message_new`` and ``message_update``
115        methods (calling ``super``) to add model-specific behavior at
116        creation and update of a thread.
117        
118        #TODO: UPDATE WITH SUBTYPE / NEW FOLLOW MECHANISM
119     '''
120     _name = 'mail.thread'
121     _description = 'Email Thread'
122
123     def _get_message_data(self, cr, uid, ids, field_names, args, context=None):
124         res = dict.fromkeys(ids)
125         for id in ids:
126             res[id] = {'message_ids': self.message_search(cr, uid, [id], context=context)}
127         for thread in self.browse(cr, uid, ids, context=context):
128             message_follower_ids = [follower.id for follower in thread.message_follower_ids]
129             res[thread.id].update({
130                 'message_is_follower': uid in message_follower_ids,
131                 'message_summary': "<span><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % 
132                     (len(res[thread.id]['message_ids']), len(thread.message_follower_ids))
133                 })
134         return res
135
136     def _search_message_ids(self, cr, uid, obj, name, args, context=None):
137         msg_obj = self.pool.get('mail.message')
138         msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
139         return [('id', 'in', msg_ids)]
140
141     _columns = {
142         'message_ids': fields.function(_get_message_data,
143                         fnct_search=_search_message_ids,
144             type='one2many', obj='mail.message', _fields_id = 'res_id',
145             string='Messages', multi="_get_message_data",
146             help="Field holding discussion about the current document."),
147         'message_follower_ids': many2many_reference('res.users',
148             rel='mail_followers', id1='res_id', id2='user_id', string="Followers",
149             help="Followers of the document. The followers have full access to " \
150                  "the document details, as well as the conversation."),
151         'message_is_follower': fields.function(_get_message_data, method=True,
152             type='boolean', string='I am Follower', multi='_get_message_data',
153             help='True if the current user is following the current document.'),
154         'message_state': fields.boolean('Read',
155             help="When checked, new messages require your attention."),
156         'message_summary': fields.function(_get_message_data, method=True,
157             type='text', string='Summary', multi='_get_message_data',
158             help="Holds the Chatter summary (number of messages, ...). "\
159                  "This summary is directly in html format in order to "\
160                  "be inserted in kanban views."),
161     }
162
163     _defaults = {
164         'message_state': True,
165     }
166
167     #------------------------------------------------------
168     # Automatic subscription when creating/reading
169     #------------------------------------------------------
170
171     def create(self, cr, uid, vals, context=None):
172         """ Override of create to subscribe :
173             - the writer
174             - followers given by the monitored fields
175         """
176         thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
177         followers_command = self.message_get_automatic_followers(cr, uid, thread_id, vals, fetch_missing=False, context=context)
178         if followers_command:
179             self.write(cr, uid, [thread_id], {'message_follower_ids': followers_command}, context=context)
180         return thread_id
181
182     def write(self, cr, uid, ids, vals, context=None):
183         """ Override of write to subscribe :
184             - the writer
185             - followers given by the monitored fields
186         """
187         if isinstance(ids, (int, long)):
188             ids = [ids]
189         for id in ids:
190             # copy original vals because we are going to modify it
191             specific_vals = dict(vals)
192             # we modify followers: do not subscribe the uid
193             if specific_vals.get('message_follower_ids'):
194                 followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, add_uid=False, context=context)
195                 specific_vals['message_follower_ids'] += followers_command
196             else:
197                 followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, context=context)
198                 specific_vals['message_follower_ids'] = followers_command
199             write_res = super(mail_thread, self).write(cr, uid, ids, specific_vals, context=context)
200         return True
201
202     def unlink(self, cr, uid, ids, context=None):
203         """Override unlink, to automatically delete messages
204            that are linked with res_model and res_id, not through
205            a foreign key with a 'cascade' ondelete attribute.
206            Notifications will be deleted with messages
207         """
208         msg_obj = self.pool.get('mail.message')
209         # delete messages and notifications
210         msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
211         msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
212         return super(mail_thread, self).unlink(cr, uid, ids, context=context)
213
214     def message_get_automatic_followers(self, cr, uid, id, record_vals, add_uid=True, fetch_missing=False, context=None):
215         """ Return the command for the many2many follower_ids field to manage
216             subscribers. Behavior :
217             - get the monitored fields (ex: ['user_id', 'responsible_id']); those
218               fields should be relationships to res.users (#TODO: res.partner)
219             - if this field is in the record_vals: it means it has been modified
220               thus add its value to the followers
221             - if this fields is not in record_vals, but fetch_missing paramter
222               is set to True: fetch the value in the record (use: at creation
223               for default values, not present in record_vals)
224             - if add_uid: add the current user (for example: writer is subscriber)
225             - generate the command and return it
226             This method has to be used on 1 id, because otherwise it would imply
227             to track which user.id is used for which record.id.
228
229             :param record_vals: values given to the create method of the new
230                 record, or values updated in a write.
231             :param monitored_fields: a list of fields that are monitored. Those
232                 fields must be many2one fields to the res.users model.
233             :param fetch_missing: is set to True, the method will read the
234                 record to find values that are not present in record_vals.
235
236             #TODO : UPDATE WHEN MERGING TO PARTNERS
237         """
238         # get monitored fields
239         monitored_fields = self.message_get_monitored_follower_fields(cr, uid, [id], context=context)
240         modified_fields = [field for field in monitored_fields if field in record_vals.iterkeys()]
241         other_fields = [field for field in monitored_fields if field not in record_vals.iterkeys()] if fetch_missing else []
242         # for each monitored field: if in record_vals, it has been modified/added
243         follower_ids = []
244         for field in modified_fields:
245             # do not add 'False'
246             if record_vals.get(fields):
247                 follower_ids.append(record_vals.get(field))
248         # for other fields: read in record if fetch_missing (otherwise list is void)
249         for field in other_fields:
250             record = self.browse(cr, uid, id, context=context)
251             value = getattr(record, field)
252             if value:
253                 follower_ids.append(value)
254         # add uid if asked and not already present
255         if add_uid and uid not in follower_ids:
256             follower_ids.append(uid)
257         return self.message_subscribe_get_command(cr, uid, follower_ids, context=context)
258
259     #------------------------------------------------------
260     # mail.message wrappers and tools
261     #------------------------------------------------------
262
263     def message_create(self, cr, uid, thread_id, vals, context=None):
264         """ OpenChatter: wrapper of mail.message create method
265            - creates the mail.message
266            - automatically subscribe the message writer
267            - push the message to followers
268         """
269         if context is None:
270             context = {}
271
272         # create message
273         msg_id = self.pool.get('mail.message').create(cr, uid, vals, context=context)
274
275         # automatically subscribe the writer of the message
276         if vals.get('user_id'):
277             record = self.browse(cr, uid, thread_id, context=context)
278             follower_ids = [follower.id for follower in record.message_follower_ids]
279             if vals.get('user_id') not in follower_ids:
280                 self.message_subscribe(cr, uid, [thread_id], [vals.get('user_id')], context=context)
281
282         # Set as unread if writer is not the document responsible
283         self.message_create_set_unread(cr, uid, [thread_id], context=context)
284         
285         # special: if install mode, do not push demo data
286         if context.get('install_mode', False):
287             return msg_id
288         
289         # get users that will get a notification pushed
290         notification_obj = self.pool.get('mail.notification')
291         user_to_push_ids = self.message_get_user_ids_to_notify(cr, uid, [thread_id], vals, context=context)
292         for id in user_to_push_ids:
293             notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
294         
295         # create the email to send
296         self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
297         
298         return msg_id
299     
300     def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
301         # get body
302         body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
303         
304         # get subscribers
305         subscr_obj = self.pool.get('mail.followers')
306         subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', thread_ids)], context=context)
307         notif_user_ids = [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
308     
309         # add users requested to perform an action (need_action mechanism)
310         if hasattr(self, 'get_needaction_user_ids') and self._columns.get('user_id'):
311             user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context)
312             for id, user_ids in user_ids_dict.iteritems():
313                 notif_user_ids += user_ids
314         
315         # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
316         if new_msg_vals.get('parent_id'):
317             notif_obj = self.pool.get('mail.notification')
318             parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
319             parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
320             notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
321
322         # remove duplicate entries
323         notif_user_ids = list(set(notif_user_ids))
324         return notif_user_ids
325
326     #------------------------------------------------------
327     # Generic message api
328     #------------------------------------------------------
329
330     def message_capable_models(self, cr, uid, context=None):
331         ret_dict = {}
332         for model_name in self.pool.obj_list():
333             model = self.pool.get(model_name)
334             if 'mail.thread' in getattr(model, '_inherit', []):
335                 ret_dict[model_name] = model._description
336         return ret_dict
337
338     def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
339                         type='email', email_date=None, parent_id=False,
340                         content_subtype='plain', state=None,
341                         partner_ids=None, email_from=False, email_to=False,
342                         email_cc=None, email_bcc=None, reply_to=None,
343                         headers=None, message_id=False, references=None,
344                         attachments=None, original=None, context=None):
345         """ Creates a new mail.message through message_create. The new message
346             is attached to the current mail.thread, containing all the details 
347             passed as parameters. All attachments will be attached to the 
348             thread record as well as to the actual message.
349            
350             This method calls message_create that will handle management of
351             subscription and notifications, and effectively create the message.
352            
353             If ``email_from`` is not set or ``type`` not set as 'email',
354             a note message is created (comment or system notification), 
355             without the usual envelope attributes (sender, recipients, etc.).
356
357             :param threads: list of thread ids, or list of browse_records
358                 representing threads to which a new message should be attached
359             :param subject: subject of the message, or description of the event;
360                 this is totally optional as subjects are not important except
361                 for specific messages (blog post, job offers) or for emails
362             :param body_text: plaintext contents of the mail or log message
363             :param body_html: html contents of the mail or log message
364             :param type: type of message: 'email', 'comment', 'notification';
365                 email by default
366             :param email_date: email date string if different from now, in
367                 server timezone
368             :param parent_id: id of the parent message (threaded messaging model)
369             :param content_subtype: optional content_subtype of message: 'plain'
370                 or 'html', corresponding to the main body contents (body_text or
371                 body_html).
372             :param state: state of message
373             :param partner_ids: destination partners of the message, in addition
374                 to the now fully optional email_to; this method is supposed to
375                 received a list of ids is not None. The specific many2many
376                 instruction will be generated by this method.
377             :param email_from: Email From / Sender address if any
378             :param email_to: Email-To / Recipient address
379             :param email_cc: Comma-Separated list of Carbon Copy Emails To
380                 addresses if any
381             :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To
382                 addresses if any
383             :param reply_to: reply_to header
384             :param headers: mail headers to store
385             :param message_id: optional email identifier
386             :param references: optional email references
387             :param dict attachments: map of attachment filenames to binary
388                 contents, if any.
389             :param str original: optional full source of the RFC2822 email, for
390                 reference
391             :param dict context: if a ``thread_model`` value is present in the
392                 context, its value will be used to determine the model of the
393                 thread to update (instead of the current model).
394         """
395         if context is None:
396             context = {}
397         if attachments is None:
398             attachments = {}
399
400         if email_date:
401             edate = parsedate(email_date)
402             if edate is not None:
403                 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
404
405         if all(isinstance(thread_id, (int, long)) for thread_id in threads):
406             model = context.get('thread_model') or self._name
407             model_pool = self.pool.get(model)
408             threads = model_pool.browse(cr, uid, threads, context=context)
409
410         ir_attachment = self.pool.get('ir.attachment')
411
412         new_msg_ids = []
413         for thread in threads:
414             to_attach = []
415             for attachment in attachments:
416                 fname, fcontent = attachment
417                 if isinstance(fcontent, unicode):
418                     fcontent = fcontent.encode('utf-8')
419                 data_attach = {
420                     'name': fname,
421                     'datas': base64.b64encode(str(fcontent)),
422                     'datas_fname': fname,
423                     'description': _('Mail attachment'),
424                     'res_model': thread._name,
425                     'res_id': thread.id,
426                 }
427                 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
428             # find related partner: partner_id column in thread object, or self is res.partner model
429             partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
430             if not partner_id and thread._name == 'res.partner':
431                 partner_id = thread.id
432             # destination partners
433             if partner_ids is None:
434                 partner_ids = []
435             mail_partner_ids = [(6, 0, partner_ids)]
436
437             data = {
438                 'subject': subject,
439                 'body_text': body_text or thread._model._columns.get('description') and thread.description or '',
440                 'body_html': body_html or '',
441                 'parent_id': parent_id,
442                 'date': email_date or fields.datetime.now(),
443                 'type': type,
444                 'content_subtype': content_subtype,
445                 'state': state,
446                 'message_id': message_id,
447                 'partner_ids': mail_partner_ids,
448                 'attachment_ids': [(6, 0, to_attach)],
449                 'user_id': uid,
450                 'model' : thread._name,
451                 'res_id': thread.id,
452                 'partner_id': partner_id,
453             }
454
455             if email_from or type == 'email':
456                 for param in (email_to, email_cc, email_bcc):
457                     if isinstance(param, list):
458                         param = ", ".join(param)
459                 data.update({
460                     'email_to': email_to,
461                     'email_from': email_from or \
462                         thread._model._columns.get('user_id') and thread.user_id and thread.user_id.user_email,
463                     'email_cc': email_cc,
464                     'email_bcc': email_bcc,
465                     'references': references,
466                     'headers': headers,
467                     'reply_to': reply_to,
468                     'original': original, })
469
470             new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
471         return new_msg_ids
472
473     def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
474         """Creates a new mail.message attached to the given threads (``ids``),
475            with the contents of ``msg_dict``, by calling ``message_append``
476            with the mail details. All attachments in msg_dict will be
477            attached to the object record as well as to the actual
478            mail message.
479
480            :param dict msg_dict: a map containing the email details and
481                                  attachments. See ``message_process()`` and
482                                 ``mail.message.parse()`` for details on
483                                 the dict structure.
484            :param dict context: if a ``thread_model`` value is present
485                                 in the context, its value will be used
486                                 to determine the model of the thread to
487                                 update (instead of the current model).
488         """
489         return self.message_append(cr, uid, ids,
490                             subject = msg_dict.get('subject'),
491                             body_text = msg_dict.get('body_text'),
492                             body_html= msg_dict.get('body_html'),
493                             parent_id = msg_dict.get('parent_id', False),
494                             type = msg_dict.get('type', 'email'),
495                             content_subtype = msg_dict.get('content_subtype'),
496                             state = msg_dict.get('state'),
497                             partner_ids = msg_dict.get('partner_ids'),
498                             email_from = msg_dict.get('from', msg_dict.get('email_from')),
499                             email_to = msg_dict.get('to', msg_dict.get('email_to')),
500                             email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
501                             email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
502                             reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
503                             email_date = msg_dict.get('date'),
504                             message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
505                             references = msg_dict.get('references')\
506                                       or msg_dict.get('in-reply-to'),
507                             attachments = msg_dict.get('attachments'),
508                             headers = msg_dict.get('headers'),
509                             original = msg_dict.get('original'),
510                             context = context)
511
512     #------------------------------------------------------
513     # Message loading
514     #------------------------------------------------------
515
516     def _message_search_ancestor_ids(self, cr, uid, ids, child_ids, ancestor_ids, context=None):
517         """ Given message child_ids ids, find their ancestors until ancestor_ids
518             using their parent_id relationship.
519
520             :param child_ids: the first nodes of the search
521             :param ancestor_ids: list of ancestors. When the search reach an
522                                  ancestor, it stops.
523         """
524         def _get_parent_ids(message_list, ancestor_ids, child_ids):
525             """ Tool function: return the list of parent_ids of messages
526                 contained in message_list. Parents that are in ancestor_ids
527                 or in child_ids are not returned. """
528             return [message['parent_id'][0] for message in message_list
529                         if message['parent_id']
530                         and message['parent_id'][0] not in ancestor_ids
531                         and message['parent_id'][0] not in child_ids
532                     ]
533
534         message_obj = self.pool.get('mail.message')
535         messages_temp = message_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
536         parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
537         child_ids += parent_ids
538         cur_iter = 0; max_iter = 100; # avoid infinite loop
539         while (parent_ids and (cur_iter < max_iter)):
540             cur_iter += 1
541             messages_temp = message_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
542             parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
543             child_ids += parent_ids
544         if (cur_iter > max_iter):
545             _logger.warning("Possible infinite loop in _message_search_ancestor_ids. "\
546                 "Note that this algorithm is intended to check for cycle in "\
547                 "message graph, leading to a curious error. Have fun.")
548         return child_ids
549
550     def message_search_get_domain(self, cr, uid, ids, context=None):
551         """ OpenChatter feature: get the domain to search the messages related
552             to a document. mail.thread defines the default behavior as
553             being messages with model = self._name, id in ids.
554             This method should be overridden if a model has to implement a
555             particular behavior.
556         """
557         return ['&', ('res_id', 'in', ids), ('model', '=', self._name)]
558
559     def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None, 
560                         limit=100, offset=0, domain=None, count=False, context=None):
561         """ OpenChatter feature: return thread messages ids according to the
562             search domain given by ``message_search_get_domain``.
563             
564             It is possible to add in the search the parent of messages by
565             setting the fetch_ancestors flag to True. In that case, using
566             the parent_id relationship, the method returns the id list according
567             to the search domain, but then calls ``_message_search_ancestor_ids``
568             that will add to the list the ancestors ids. The search is limited
569             to parent messages having an id in ancestor_ids or having
570             parent_id set to False.
571             
572             If ``count==True``, the number of ids is returned instead of the
573             id list. The count is done by hand instead of passing it as an 
574             argument to the search call because we might want to perform
575             a research including parent messages until some ancestor_ids.
576             
577             :param fetch_ancestors: performs an ascended search; will add 
578                                     to fetched msgs all their parents until
579                                     ancestor_ids
580             :param ancestor_ids: used when fetching ancestors
581             :param domain: domain to add to the search; especially child_of
582                            is interesting when dealing with threaded display.
583                            Note that the added domain is anded with the 
584                            default domain.
585             :param limit, offset, count, context: as usual
586         """
587         search_domain = self.message_search_get_domain(cr, uid, ids, context=context)
588         if domain:
589             search_domain += domain
590         message_obj = self.pool.get('mail.message')
591         message_res = message_obj.search(cr, uid, search_domain, limit=limit, offset=offset, count=count, context=context)
592         if not count and fetch_ancestors:
593             message_res += self._message_search_ancestor_ids(cr, uid, ids, message_res, ancestor_ids, context=context) 
594         return message_res
595
596     def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None, 
597                         limit=100, offset=0, domain=None, context=None):
598         """ OpenChatter feature: read the messages related to some threads.
599             This method is used mainly the Chatter widget, to directly have
600             read result instead of searching then reading.
601
602             Please see message_search for more information about the parameters.
603         """
604         message_ids = self.message_search(cr, uid, ids, fetch_ancestors, ancestor_ids,
605             limit, offset, domain, context=context)
606         messages = self.pool.get('mail.message').read(cr, uid, message_ids, context=context)
607
608         """ Retrieve all attachments names """
609         map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
610
611         ids = map_id_to_name.keys()
612         names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
613         
614         # convert the list of tuples into a dictionnary
615         for name in names: 
616             map_id_to_name[name[0]] = name[1]
617         
618         # give corresponding ids and names to each message
619         for msg in messages:
620             msg["attachments"] = []
621             
622             for attach_id in msg["attachment_ids"]:
623                 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
624         
625         # Set the threads as read
626         self.message_check_and_set_read(cr, uid, ids, context=context)
627         # Sort and return the messages
628         messages = sorted(messages, key=lambda d: (-d['id']))
629         return messages
630
631     def message_get_pushed_messages(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
632                             limit=100, offset=0, msg_search_domain=[], context=None):
633         """ OpenChatter: wall: get the pushed notifications and used them
634             to fetch messages to display on the wall.
635             
636             :param fetch_ancestors: performs an ascended search; will add
637                                     to fetched msgs all their parents until
638                                     ancestor_ids
639             :param ancestor_ids: used when fetching ancestors
640             :param domain: domain to add to the search; especially child_of
641                            is interesting when dealing with threaded display
642             :param ascent: performs an ascended search; will add to fetched msgs
643                            all their parents until root_ids
644             :param root_ids: for ascent search
645             :return: list of mail.messages sorted by date
646         """
647         notification_obj = self.pool.get('mail.notification')
648         msg_obj = self.pool.get('mail.message')
649         # update message search
650         for arg in msg_search_domain:
651             if isinstance(arg, (tuple, list)):
652                 arg[0] = 'message_id.' + arg[0]
653         # compose final domain
654         domain = [('user_id', '=', uid)] + msg_search_domain
655         # get notifications
656         notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
657         notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
658         msg_ids = [notification.message_id.id for notification in notifications]
659         # get messages
660         msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
661         if (fetch_ancestors): msg_ids = self._message_search_ancestor_ids(cr, uid, ids, msg_ids, ancestor_ids, context=context)
662         msgs = msg_obj.read(cr, uid, msg_ids, context=context)
663         return msgs
664
665     def _message_find_user_id(self, cr, uid, message, context=None):
666         from_local_part = to_email(decode(message.get('From')))[0]
667         user_ids = self.pool.get('res.users').search(cr, uid, [('login', '=', from_local_part)], context=context)
668         return user_ids[0] if user_ids else uid
669
670     #------------------------------------------------------
671     # Mail gateway
672     #------------------------------------------------------
673     # message_process will call either message_new or message_update.
674
675     def message_route(self, cr, uid, message, model=None, thread_id=None,
676                       custom_values=None, context=None):
677         """Attempt to figure out the correct target model, thread_id,
678         custom_values and user_id to use for an incoming message.
679         Multiple values may be returned, if a message had multiple
680         recipients matching existing mail.aliases, for example.
681
682         The following heuristics are used, in this order: 
683              1. If the message replies to an existing thread_id, and
684                 properly contains the thread model in the 'In-Reply-To'
685                 header, use this model/thread_id pair, and ignore
686                 custom_value (not needed as no creation will take place) 
687              2. Look for a mail.alias entry matching the message
688                 recipient, and use the corresponding model, thread_id,
689                 custom_values and user_id.
690              3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
691                 provided.
692              4. If all the above fails, raise an exception.
693
694            :param string message: an email.message instance
695            :param string model: the fallback model to use if the message
696                does not match any of the currently configured mail aliases
697                (may be None if a matching alias is supposed to be present)
698            :type dict custom_values: optional dictionary of default field values
699                 to pass to ``message_new`` if a new record needs to be created.
700                 Ignored if the thread record already exists, and also if a
701                 matching mail.alias was found (aliases define their own defaults)
702            :param int thread_id: optional ID of the record/thread from ``model``
703                to which this mail should be attached. Only used if the message
704                does not reply to an existing thread and does not match any mail alias.
705            :return: list of [model, thread_id, custom_values, user_id]
706         """
707         assert isinstance(message, Message), 'message must be an email.message.Message at this point'
708         message_id = message.get('Message-Id')
709
710         # 1. Verify if this is a reply to an existing thread
711         references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To')
712         ref_match = references and tools.reference_re.search(references)
713         if ref_match:
714             thread_id = int(ref_match.group(1))
715             model = ref_match.group(2) or model
716             model_pool = self.pool.get(model)
717             if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
718                 and hasattr(model_pool, 'message_update'):
719                 _logger.debug('Routing mail with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
720                               message_id, model, thread_id, custom_values, uid)
721                 return [(model, thread_id, custom_values, uid)]
722         
723         # 2. Look for a matching mail.alias entry
724         # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
725         # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
726         rcpt_tos = decode_header(message, 'Delivered-To') or \
727              ','.join([decode_header(message, 'To'),
728                        decode_header(message, 'Cc'),
729                        decode_header(message, 'Resent-To'),
730                        decode_header(message, 'Resent-Cc')])
731         local_parts = [e.split('@')[0] for e in to_email(rcpt_tos)]
732         if local_parts:
733             mail_alias = self.pool.get('mail.alias')
734             alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
735             if alias_ids:
736                 routes = []
737                 for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
738                     user_id = alias.alias_user_id.id
739                     if not user_id:
740                         user_id = self._message_find_user_id(cr, uid, message, context=context)
741                     routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
742                                    eval(alias.alias_defaults), user_id))
743                 _logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
744                 return routes
745         
746         # 3. Fallback to the provided parameters, if they work
747         model_pool = self.pool.get(model)
748         if not thread_id:
749             # Legacy: fallback to matching [ID] in the Subject
750             match = tools.res_re.search(decode_header(message, 'Subject'))
751             thread_id = match and match.group(1)
752         assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
753             "No possible route found for incoming message with Message-Id %s. " \
754             "Create an appropriate mail.alias or force the destination model." % message_id
755         if thread_id and not model_pool.exists(cr, uid, thread_id):
756             _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
757                             thread_id, message_id)
758             thread_id = None
759         _logger.debug('Routing mail with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
760                       message_id, model, thread_id, custom_values, uid)
761         return [(model, thread_id, custom_values, uid)]
762
763
764     def message_process(self, cr, uid, model, message, custom_values=None,
765                         save_original=False, strip_attachments=False,
766                         thread_id=None, context=None):
767         """Process an incoming RFC2822 email message, relying on
768            ``mail.message.parse()`` for the parsing operation,
769            and ``message_route()`` to figure out the target model. 
770            
771            Once the target model is known, its ``message_new`` method
772            is called with the new message (if the thread record did not exist)
773             or its ``message_update`` method (if it did). Finally,
774            ``message_forward`` is called to automatically notify other
775            people that should receive this message.
776
777            :param string model: the fallback model to use if the message
778                does not match any of the currently configured mail aliases
779                (may be None if a matching alias is supposed to be present)
780            :param message: source of the RFC2822 message
781            :type message: string or xmlrpclib.Binary
782            :type dict custom_values: optional dictionary of field values
783                 to pass to ``message_new`` if a new record needs to be created.
784                 Ignored if the thread record already exists, and also if a
785                 matching mail.alias was found (aliases define their own defaults)
786            :param bool save_original: whether to keep a copy of the original
787                 email source attached to the message after it is imported.
788            :param bool strip_attachments: whether to strip all attachments
789                 before processing the message, in order to save some space.
790            :param int thread_id: optional ID of the record/thread from ``model``
791                to which this mail should be attached. When provided, this
792                overrides the automatic detection based on the message
793                headers.
794         """
795         if context is None: context = {}
796
797         # extract message bytes - we are forced to pass the message as binary because
798         # we don't know its encoding until we parse its headers and hence can't
799         # convert it to utf-8 for transport between the mailgate script and here.
800         if isinstance(message, xmlrpclib.Binary):
801             message = str(message.data)
802         # Warning: message_from_string doesn't always work correctly on unicode,
803         # we must use utf-8 strings here :-(
804         if isinstance(message, unicode):
805             message = message.encode('utf-8')
806         msg_txt = email.message_from_string(message)
807         routes = self.message_route(cr, uid, msg_txt, model,
808                                     thread_id, custom_values,
809                                     context=context)
810         msg = self.pool.get('mail.message').parse_message(msg_txt, save_original=save_original, context=context)
811         msg['state'] = 'received'     
812         if strip_attachments and 'attachments' in msg:
813             del msg['attachments']
814         for model, thread_id, custom_values, user_id in routes:   
815             if self._name != model:
816                 context.update({'thread_model': model})
817             model_pool = self.pool.get(model)
818             assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
819                 "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
820                     (msg['message-id'], model)
821             if thread_id and hasattr(model_pool, 'message_update'):
822                 model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
823             else:
824                 thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
825     
826             # Forward the email to other followers
827             self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context)
828             model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
829         return True
830
831     def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
832         """Called by ``message_process`` when a new message is received
833            for a given thread model, if the message did not belong to
834            an existing thread.
835            The default behavior is to create a new record of the corresponding
836            model (based on some very basic info extracted from the message),
837            then attach the message to the newly created record
838            (by calling ``message_append_dict``).
839            Additional behavior may be implemented by overriding this method.
840
841            :param dict msg_dict: a map containing the email details and
842                                  attachments. See ``message_process`` and
843                                 ``mail.message.parse`` for details.
844            :param dict custom_values: optional dictionary of additional
845                                       field values to pass to create()
846                                       when creating the new thread record.
847                                       Be careful, these values may override
848                                       any other values coming from the message.
849            :param dict context: if a ``thread_model`` value is present
850                                 in the context, its value will be used
851                                 to determine the model of the record
852                                 to create (instead of the current model).
853            :rtype: int
854            :return: the id of the newly created thread object
855         """
856         if context is None:
857             context = {}
858         model = context.get('thread_model') or self._name
859         model_pool = self.pool.get(model)
860         fields = model_pool.fields_get(cr, uid, context=context)
861         data = model_pool.default_get(cr, uid, fields, context=context)
862         if 'name' in fields and not data.get('name'):
863             data['name'] = msg_dict.get('subject', '')
864         if custom_values and isinstance(custom_values, dict):
865             data.update(custom_values)
866         res_id = model_pool.create(cr, uid, data, context=context)
867         self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
868         return res_id
869
870     def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
871         """Called by ``message_process`` when a new message is received
872            for an existing thread. The default behavior is to create a
873            new mail.message in the given thread (by calling
874            ``message_append_dict``)
875            Additional behavior may be implemented by overriding this
876            method.
877            :param dict msg_dict: a map containing the email details and
878                                attachments. See ``message_process`` and
879                                ``mail.message.parse()`` for details.
880            :param dict update_vals: a dict containing values to update records
881                               given their ids; if the dict is None or is
882                               void, no write operation is performed.
883         """
884         if update_vals:
885             self.write(cr, uid, ids, update_vals, context=context)
886         return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
887
888     def message_thread_followers(self, cr, uid, ids, context=None):
889         """ Returns a list of email addresses of the people following
890             this thread, including the sender of each mail, and the
891             people who were in CC of the messages, if any.
892         """
893         res = {}
894         if isinstance(ids, (str, int, long)):
895             ids = [long(ids)]
896         for thread in self.browse(cr, uid, ids, context=context):
897             l = set()
898             for message in thread.message_ids:
899                 l.add((message.user_id and message.user_id.email) or '')
900                 l.add(message.email_from or '')
901                 l.add(message.email_cc or '')
902             res[thread.id] = filter(None, l)
903         return res
904
905     def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
906         """Sends an email to all people following the given threads.
907            The emails are forwarded immediately, not queued for sending,
908            and not archived.
909
910         :param str model: thread model
911         :param list thread_ids: ids of the thread records
912         :param msg: email.message.Message object to forward
913         :param email_error: optional email address to notify in case
914                             of any delivery error during the forward.
915         :return: True
916         """
917         model_pool = self.pool.get(model)
918         smtp_server_obj = self.pool.get('ir.mail_server')
919         for res in model_pool.browse(cr, uid, thread_ids, context=context):
920             if hasattr(model_pool, 'message_thread_followers'):
921                 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
922             else:
923                 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
924             message_followers_emails = to_email(','.join(filter(None, followers)))
925             message_recipients = to_email(','.join(filter(None,
926                                                                        [decode(msg['from']),
927                                                                         decode(msg['to']),
928                                                                         decode(msg['cc'])])))
929             forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
930             if forward_to:
931                 # TODO: we need an interface for this for all types of objects, not just leads
932                 if model_pool._columns.get('section_id'):
933                     del msg['reply-to']
934                     msg['reply-to'] = res.section_id.reply_to
935
936                 smtp_from, = to_email(msg['from'])
937                 msg['from'] = smtp_from
938                 msg['to'] =  ", ".join(forward_to)
939                 msg['message-id'] = tools.generate_tracking_message_id(res.id)
940                 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
941                     subj = msg['subject']
942                     del msg['subject'], msg['to'], msg['cc'], msg['bcc']
943                     msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
944                     msg['to'] = email_error
945                     smtp_server_obj.send_email(cr, uid, msg)
946         return True
947
948     def message_partner_by_email(self, cr, uid, email, context=None):
949         """Attempts to return the id of a partner address matching
950            the given ``email``, and the corresponding partner id.
951            Can be used by classes using the ``mail.thread`` mixin
952            to lookup the partner and use it in their implementation
953            of ``message_new`` to link the new record with a
954            corresponding partner.
955            The keys used in the returned dict are meant to map
956            to usual names for relationships towards a partner
957            and one of its addresses.
958
959            :param email: email address for which a partner
960                          should be searched for.
961            :rtype: dict
962            :return: a map of the following form::
963
964                       { 'partner_address_id': id or False,
965                         'partner_id': pid or False }
966         """
967         partner_pool = self.pool.get('res.partner')
968         res = {'partner_id': False}
969         if email:
970             email = to_email(email)[0]
971             contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
972             if contact_ids:
973                 contact = partner_pool.browse(cr, uid, contact_ids[0])
974                 res['partner_id'] = contact.id
975         return res
976
977     # for backwards-compatibility with old scripts
978     process_email = message_process
979
980     #------------------------------------------------------
981     # Note specific
982     #------------------------------------------------------
983
984     def log(self, cr, uid, id, message, secondary=False, context=None):
985         _logger.warning("log() is deprecated. As this module inherit from \
986                         mail.thread, the message will be managed by this \
987                         module instead of by the res.log mechanism. Please \
988                         use the mail.thread OpenChatter API instead of the \
989                         now deprecated res.log.")
990         self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
991
992     def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False,
993                             type='notification', content_subtype='html', context=None):
994         if content_subtype == 'html':
995             body_html = body
996             body_text = body
997         else:
998             body_html = body
999             body_text = body
1000         return self.message_append(cr, uid, ids, subject, body_html, body_text,
1001                                     type, parent_id=parent_id,
1002                                     content_subtype=content_subtype, context=context)
1003
1004     #------------------------------------------------------
1005     # Subscription mechanism
1006     #------------------------------------------------------
1007
1008     def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
1009         """ Returns a list of fields containing a res.user.id. Those fields
1010             will be checked to automatically subscribe those users.
1011         """
1012         return []
1013
1014     def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
1015         """ Subscribe the user (or user_ids) to the current document.
1016             
1017             :param user_ids: a list of user_ids; if not set, subscribe
1018                              uid instead
1019             :param return: new value of followers, for Chatter
1020         """
1021         to_subscribe_uids = [uid] if user_ids is None else user_ids
1022         write_res = self.write(cr, uid, ids, {'message_follower_ids': self.message_subscribe_get_command(cr, uid, to_subscribe_uids, context)}, context=context)
1023         return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
1024
1025     def message_subscribe_get_command(self, cr, uid, follower_ids, context=None):
1026         """ Generate the many2many command to add followers. """
1027         return [(4, id) for id in follower_ids]
1028
1029     def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
1030         """ Unsubscribe the user (or user_ids) from the current document.
1031             
1032             :param user_ids: a list of user_ids; if not set, subscribe
1033                              uid instead
1034             :param return: new value of followers, for Chatter
1035         """
1036         to_unsubscribe_uids = [uid] if user_ids is None else user_ids
1037         write_res = self.write(cr, uid, ids, {'message_follower_ids': self.message_unsubscribe_get_command(cr, uid, to_unsubscribe_uids, context)}, context=context)
1038         return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
1039
1040     def message_unsubscribe_get_command(self, cr, uid, follower_ids, context=None):
1041         """ Generate the many2many command to remove followers. """
1042         return [(3, id) for id in follower_ids]
1043
1044     #------------------------------------------------------
1045     # Notification API
1046     #------------------------------------------------------
1047
1048     def message_create_notify_by_email(self, cr, uid, new_msg_values, user_to_notify_ids, context=None):
1049         """ When creating a new message and pushing notifications, emails
1050             must be send if users have chosen to receive notifications
1051             by email via the notification_email_pref field.
1052             
1053             ``notification_email_pref`` can have 3 values :
1054             - all: receive all notification by email (for example for shared
1055               users)
1056             - to_me: messages send directly to me (@login, messages on res.users)
1057             - never: never receive notifications
1058             Note that an user should never receive notifications for messages
1059             he has created.
1060             
1061             :param new_msg_values: dictionary of message values, those that
1062                                    are given to the create method
1063             :param user_to_notify_ids: list of user_ids, user that will
1064                                        receive a notification on their Wall
1065         """
1066         message_obj = self.pool.get('mail.message')
1067         res_users_obj = self.pool.get('res.users')
1068         body = new_msg_values.get('body_html', '') if new_msg_values.get('content_subtype') == 'html' else new_msg_values.get('body_text', '')
1069         
1070         # remove message writer
1071         if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0:
1072             user_to_notify_ids.remove(new_msg_values.get('user_id'))
1073
1074         # try to find an email_to
1075         email_to = ''
1076         for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context):
1077             # TO BE REFACTORED BY FP, JUSTE REMOVED TO_ME, NOT SURE WHAT S NEW BEHAVIOR
1078             if not user.notification_email_pref == 'all':
1079                 continue
1080             if not user.email:
1081                 continue
1082             email_to = '%s, %s' % (email_to, user.email)
1083             email_to = email_to.lstrip(', ')
1084         
1085         # did not find any email address: not necessary to create an email
1086         if not email_to:
1087             return
1088         
1089         # try to find an email_from
1090         current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
1091         email_from = new_msg_values.get('email_from')
1092         if not email_from:
1093             email_from = current_user.email
1094         
1095         # get email content, create it (with mail_message.create)
1096         email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
1097         email_id = message_obj.create(cr, uid, email_values, context=context)
1098         return email_id
1099     
1100     def message_create_notify_get_email_dict(self, cr, uid, new_msg_values, email_from, email_to, context=None):
1101         values = dict(new_msg_values)
1102         
1103         body_html = new_msg_values.get('body_html', '')
1104         if body_html:
1105             body_html += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1106         body_text = new_msg_values.get('body_text', '')
1107         if body_text:
1108             body_text += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1109         values.update({
1110             'type': 'email',
1111             'state': 'outgoing',
1112             'email_from': email_from,
1113             'email_to': email_to,
1114             'subject': 'New message',
1115             'content_subtype': new_msg_values.get('content_subtype', 'plain'),
1116             'body_html': body_html,
1117             'body_text': body_text,
1118             'auto_delete': True,
1119             'res_model': '',
1120             'res_id': False,
1121         })
1122         return values
1123
1124     def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
1125         notif_obj = self.pool.get('mail.notification')
1126         msg_obj = self.pool.get('mail.message')
1127         if remove_childs:
1128             notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
1129         else:
1130             notif_msg_ids = msg_ids
1131         to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
1132         return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
1133
1134     #------------------------------------------------------
1135     # Thread_state
1136     #------------------------------------------------------
1137
1138     def message_create_set_unread(self, cr, uid, ids, context=None):
1139         """ When creating a new message, set as unread if uid is not the
1140             object responsible. """
1141         for obj in self.browse(cr, uid, ids, context=context):
1142             if obj.message_state and self._columns.get('user_id') and (not obj.user_id or obj.user_id.id != uid):
1143                 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1144
1145     def message_check_and_set_unread(self, cr, uid, ids, context=None):
1146         """ Set unread if uid is the object responsible or if the object has
1147             no responsible. """
1148         for obj in self.browse(cr, uid, ids, context=context):
1149             if obj.message_state and self._columns.get('user_id') and (not obj.user_id or obj.user_id.id == uid):
1150                 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1151
1152     def message_mark_as_unread(self, cr, uid, ids, context=None):
1153         """ Set as unread. """
1154         return self.write(cr, uid, ids, {'message_state': False}, context=context)
1155
1156     def message_check_and_set_read(self, cr, uid, ids, context=None):
1157         """ Set read if uid is the object responsible. """
1158         for obj in self.browse(cr, uid, ids, context=context):
1159             if not obj.message_state and self._columns.get('user_id') and obj.user_id and obj.user_id.id == uid:
1160                 self.message_mark_as_read(cr, uid, [obj.id], context=context)
1161     
1162     def message_mark_as_read(self, cr, uid, ids, context=None):
1163         """ Set as read. """
1164         return self.write(cr, uid, ids, {'message_state': True}, context=context)
1165
1166
1167 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: