[IMP] hr_recruitment: improved search views.
[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 from email.utils import parsedate
25 import logging
26 from mail_message import decode, to_email
27 from operator import itemgetter
28 from osv import osv, fields
29 import re
30 import time
31 import tools
32 from tools.translate import _
33 import xmlrpclib
34
35 _logger = logging.getLogger(__name__)
36
37 class mail_thread(osv.Model):
38     '''Mixin model, meant to be inherited by any model that needs to
39        act as a discussion topic on which messages can be attached.
40        Public methods are prefixed with ``message_`` in order to avoid
41        name collisions with methods of the models that will inherit
42        from this mixin.
43
44        ``mail.thread`` is designed to work without adding any field
45        to the extended models. All functionalities and expected behavior
46        are managed by mail.thread, using model name and record ids.
47        A widget has been designed for the 6.1 and following version of OpenERP
48        web-client. However, due to technical limitations, ``mail.thread``
49        adds a simulated one2many field, to display the web widget by
50        overriding the default field displayed. Using this field
51        is not recommanded has it will disappeear in future version
52        of OpenERP, leading to a pure mixin class.
53
54        Inheriting classes are not required to implement any method, as the
55        default implementation will work for any model. However it is common
56        to override at least the ``message_new`` and ``message_update``
57        methods (calling ``super``) to add model-specific behavior at
58        creation and update of a thread; and ``message_get_subscribers``
59        to manage more precisely the social aspect of the thread through
60        the followers.
61     '''
62     _name = 'mail.thread'
63     _description = 'Email Thread'
64
65     def _get_message_ids(self, cr, uid, ids, name, args, context=None):
66         res = {}
67         for id in ids:
68             message_ids = self.message_load_ids(cr, uid, [id], context=context)
69             subscriber_ids = self.message_get_subscribers(cr, uid, [id], context=context)
70             res[id] = {
71                 'message_ids': message_ids,
72                 'message_summary': "<span><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (len(message_ids), len(subscriber_ids)),
73             }
74         return res
75
76     def _search_message_ids(self, cr, uid, obj, name, args, context=None):
77         msg_obj = self.pool.get('mail.message')
78         msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
79         return [('id', 'in', msg_ids)]
80
81     _columns = {
82         'message_ids': fields.function(_get_message_ids, method=True,
83                         fnct_search=_search_message_ids,
84             type='one2many', obj='mail.message', _fields_id = 'res_id',
85             string='Temp messages', multi="_get_message_ids",
86             help="Functional field holding messages related to the current document."),
87         'message_state': fields.boolean('Read',
88             help="When checked, new messages require your attention."),
89         'message_summary': fields.function(_get_message_ids, method=True,
90             type='text', string='Summary', multi="_get_message_ids",
91             help="Holds the Chatter summary (number of messages, ...). "\
92                  "This summary is directly in html format in order to "\
93                  "be inserted in kanban views."),
94     }
95     
96     _defaults = {
97         'message_state': True,
98     }
99
100     #------------------------------------------------------
101     # Automatic subscription when creating/reading
102     #------------------------------------------------------
103
104     def create(self, cr, uid, vals, context=None):
105         """Automatically subscribe the creator """
106         thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
107         if thread_id:
108             self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
109         return thread_id
110
111     def write(self, cr, uid, ids, vals, context=None):
112         """Automatically subscribe the writer"""
113         if isinstance(ids, (int, long)):
114             ids = [ids]
115         write_res = super(mail_thread, self).write(cr, uid, ids, vals, context=context);
116         if write_res:
117             self.message_subscribe(cr, uid, ids, [uid], context=context)
118         return write_res;
119
120     def unlink(self, cr, uid, ids, context=None):
121         """Override unlink, to automatically delete
122            - subscriptions
123            - messages
124            that are linked with res_model and res_id, not through
125            a foreign key with a 'cascade' ondelete attribute.
126            Notifications will be deleted with messages
127         """
128         subscr_obj = self.pool.get('mail.subscription')
129         msg_obj = self.pool.get('mail.message')
130         # delete subscriptions
131         subscr_to_del_ids = subscr_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
132         subscr_obj.unlink(cr, uid, subscr_to_del_ids, context=context)
133         # delete notifications
134         msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
135         msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
136
137         return super(mail_thread, self).unlink(cr, uid, ids, context=context)
138
139     #------------------------------------------------------
140     # mail.message wrappers and tools
141     #------------------------------------------------------
142
143     def message_create(self, cr, uid, thread_id, vals, context=None):
144         """ OpenChatter: wrapper of mail.message create method
145            - creates the mail.message
146            - automatically subscribe the message writer
147            - push the message to subscribed users
148         """
149         if context is None:
150             context = {}
151         
152         message_obj = self.pool.get('mail.message')
153         subscription_obj = self.pool.get('mail.subscription')
154         notification_obj = self.pool.get('mail.notification')
155         res_users_obj = self.pool.get('res.users')
156         body = vals.get('body_html', '') if vals.get('subtype', 'plain') == 'html' else vals.get('body_text', '')
157
158         # automatically subscribe the writer of the message
159         if vals['user_id']:
160             self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
161
162         # get users that will get a notification pushed
163         user_to_push_ids = self.message_create_get_notification_user_ids(cr, uid, [thread_id], vals, context=context)
164         user_to_push_from_parse_ids = self.message_parse_users(cr, uid, [thread_id], body, context=context)
165
166         # set email_from and email_to for comments and notifications
167         if vals.get('type', False) and vals['type'] == 'comment' or vals['type'] == 'notification':
168             current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
169             if not vals.get('email_from', False):
170                 vals['email_from'] = current_user.user_email
171             if not vals.get('email_to', False):
172                 email_to = ''
173                 for user in res_users_obj.browse(cr, uid, user_to_push_ids, context=context):
174                     if not user.notification_email_pref == 'all' and \
175                         not (user.notification_email_pref == 'comments' and vals['type'] == 'comment') and \
176                         not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
177                         continue
178                     if not user.user_email:
179                         continue
180                     email_to = '%s, %s' % (email_to, user.user_email)
181                 email_to = email_to.lstrip(', ')
182                 if email_to:
183                     vals['email_to'] = email_to
184                     vals['state'] = 'outgoing'
185
186         # create message
187         msg_id = message_obj.create(cr, uid, vals, context=context)
188         
189         # Set as unread if writer is not the document responsible
190         self.message_create_set_unread(cr, uid, [thread_id], context=context)
191         
192         # special: if install mode, do not push demo data
193         if context.get('install_mode', False):
194             return True
195
196         # push to users
197         for id in user_to_push_ids:
198             notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
199
200         return msg_id
201
202     def message_create_get_notification_user_ids(self, cr, uid, thread_ids, new_msg_vals, context=None):
203         if context is None:
204             context = {}
205
206         notif_user_ids = []
207         body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
208         for thread_id in thread_ids:
209             # add subscribers
210             notif_user_ids += self.message_get_subscribers(cr, uid, [thread_id], context=context)
211             # add users requested via parsing message (@login)
212             notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
213             # add users requested to perform an action (need_action mechanism)
214             if hasattr(self, 'get_needaction_user_ids'):
215                 notif_user_ids += self.get_needaction_user_ids(cr, uid, [thread_id], context=context)[thread_id]
216             # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
217             if new_msg_vals.get('parent_id'):
218                 notif_obj = self.pool.get('mail.notification')
219                 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
220                 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
221                 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
222
223         # remove duplicate entries
224         notif_user_ids = list(set(notif_user_ids))
225         return notif_user_ids
226
227     def message_parse_users(self, cr, uid, ids, string, context=None):
228         """Parse message content
229            - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
230              this supports login that are emails (such as @admin@lapin.net)
231         """
232         regex = re.compile('(^|\s)@((\w|@|\.)*)')
233         login_lst = [item[1] for item in regex.findall(string)]
234         if not login_lst: return []
235         user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
236         return user_ids
237
238     #------------------------------------------------------
239     # Generic message api
240     #------------------------------------------------------
241
242     def message_capable_models(self, cr, uid, context=None):
243         ret_dict = {}
244         for model_name in self.pool.obj_list():
245             model = self.pool.get(model_name)
246             if 'mail.thread' in getattr(model, '_inherit', []):
247                 ret_dict[model_name] = model._description
248         return ret_dict
249
250     def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
251                         parent_id=False, type='email', subtype='plain', state='received',
252                         email_to=False, email_from=False, email_cc=None, email_bcc=None,
253                         reply_to=None, email_date=None, message_id=False, references=None,
254                         attachments=None, headers=None, original=None, context=None):
255         """Creates a new mail.message attached to the current mail.thread,
256            containing all the details passed as parameters.  All attachments
257            will be attached to the thread record as well as to the actual
258            message.
259            If ``email_from`` is not set or ``type`` not set as 'email',
260            a note message is created, without the usual envelope
261            attributes (sender, recipients, etc.).
262            The creation of the message is done by calling ``message_create``
263            method, that will manage automatic pushing of notifications.
264
265         :param threads: list of thread ids, or list of browse_records representing
266                         threads to which a new message should be attached
267         :param subject: subject of the message, or description of the event if this
268                         is an *event log* entry.
269         :param body_text: plaintext contents of the mail or log message
270         :param body_html: html contents of the mail or log message
271         :param parent_id: id of the parent message (threaded messaging model)
272         :param type: optional type of message: 'email', 'comment', 'notification'
273         :param subtype: optional subtype of message: 'plain' or 'html', corresponding to the main
274                         body contents (body_text or body_html).
275         :param state: optional state of message; 'received' by default
276         :param email_to: Email-To / Recipient address
277         :param email_from: Email From / Sender address if any
278         :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
279         :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
280         :param reply_to: reply_to header
281         :param email_date: email date string if different from now, in server timezone
282         :param message_id: optional email identifier
283         :param references: optional email references
284         :param headers: mail headers to store
285         :param dict attachments: map of attachment filenames to binary contents, if any.
286         :param str original: optional full source of the RFC2822 email, for reference
287         :param dict context: if a ``thread_model`` value is present
288                              in the context, its value will be used
289                              to determine the model of the thread to
290                              update (instead of the current model).
291         """
292         if context is None:
293             context = {}
294         if attachments is None:
295             attachments = {}
296
297         if email_date:
298             edate = parsedate(email_date)
299             if edate is not None:
300                 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
301
302         if all(isinstance(thread_id, (int, long)) for thread_id in threads):
303             model = context.get('thread_model') or self._name
304             model_pool = self.pool.get(model)
305             threads = model_pool.browse(cr, uid, threads, context=context)
306
307         ir_attachment = self.pool.get('ir.attachment')
308         mail_message = self.pool.get('mail.message')
309
310         new_msg_ids = []
311         for thread in threads:
312             to_attach = []
313             for attachment in attachments:
314                 fname, fcontent = attachment
315                 if isinstance(fcontent, unicode):
316                     fcontent = fcontent.encode('utf-8')
317                 data_attach = {
318                     'name': fname,
319                     'datas': base64.b64encode(str(fcontent)),
320                     'datas_fname': fname,
321                     'description': _('Mail attachment'),
322                     'res_model': thread._name,
323                     'res_id': thread.id,
324                 }
325                 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
326
327             partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
328             if not partner_id and thread._name == 'res.partner':
329                 partner_id = thread.id
330             data = {
331                 'subject': subject,
332                 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
333                 'body_html': body_html or '',
334                 'parent_id': parent_id,
335                 'date': email_date or fields.datetime.now(),
336                 'type': type,
337                 'subtype': subtype,
338                 'state': state,
339                 'message_id': message_id,
340                 'attachment_ids': [(6, 0, to_attach)],
341                 'user_id': uid,
342                 'model' : thread._name,
343                 'res_id': thread.id,
344                 'partner_id': partner_id,
345             }
346
347             if email_from or type == 'email':
348                 for param in (email_to, email_cc, email_bcc):
349                     if isinstance(param, list):
350                         param = ", ".join(param)
351                 data.update({
352                     'subject': subject or _('History'),
353                     'email_to': email_to,
354                     'email_from': email_from or \
355                         (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
356                     'email_cc': email_cc,
357                     'email_bcc': email_bcc,
358                     'references': references,
359                     'headers': headers,
360                     'reply_to': reply_to,
361                     'original': original, })
362
363             new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
364         return new_msg_ids
365
366     def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
367         """Creates a new mail.message attached to the given threads (``ids``),
368            with the contents of ``msg_dict``, by calling ``message_append``
369            with the mail details. All attachments in msg_dict will be
370            attached to the object record as well as to the actual
371            mail message.
372
373            :param dict msg_dict: a map containing the email details and
374                                  attachments. See ``message_process()`` and
375                                 ``mail.message.parse()`` for details on
376                                 the dict structure.
377            :param dict context: if a ``thread_model`` value is present
378                                 in the context, its value will be used
379                                 to determine the model of the thread to
380                                 update (instead of the current model).
381         """
382         return self.message_append(cr, uid, ids,
383                             subject = msg_dict.get('subject'),
384                             body_text = msg_dict.get('body_text'),
385                             body_html= msg_dict.get('body_html'),
386                             parent_id = msg_dict.get('parent_id', False),
387                             type = msg_dict.get('type', 'email'),
388                             subtype = msg_dict.get('subtype', 'plain'),
389                             state = msg_dict.get('state', 'received'),
390                             email_from = msg_dict.get('from', msg_dict.get('email_from')),
391                             email_to = msg_dict.get('to', msg_dict.get('email_to')),
392                             email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
393                             email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
394                             reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
395                             email_date = msg_dict.get('date'),
396                             message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
397                             references = msg_dict.get('references')\
398                                       or msg_dict.get('in-reply-to'),
399                             attachments = msg_dict.get('attachments'),
400                             headers = msg_dict.get('headers'),
401                             original = msg_dict.get('original'),
402                             context = context)
403
404     def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
405         """ Given message child_ids
406             Find their ancestors until root ids"""
407         if context is None:
408             context = {}
409         msg_obj = self.pool.get('mail.message')
410         tmp_msgs = msg_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
411         parent_ids = [msg['parent_id'][0] for msg in tmp_msgs if msg['parent_id'] and msg['parent_id'][0] not in root_ids and msg['parent_id'][0] not in child_ids]
412         child_ids += parent_ids
413         cur_iter = 0; max_iter = 100; # avoid infinite loop
414         while (parent_ids and (cur_iter < max_iter)):
415             cur_iter += 1
416             tmp_msgs = msg_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
417             parent_ids = [msg['parent_id'][0] for msg in tmp_msgs if msg['parent_id'] and msg['parent_id'][0] not in root_ids and msg['parent_id'][0] not in child_ids]
418             child_ids += parent_ids
419         if (cur_iter > max_iter):
420             _logger.warning("Possible infinite loop in _message_add_ancestor_ids. Note that this algorithm is intended to check for cycle in message graph.")
421         return child_ids
422
423     def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
424         """ OpenChatter feature: return thread messages ids. It searches in
425             mail.messages where res_id = ids, (res_)model = current model.
426             :param domain: domain to add to the search; especially child_of
427                            is interesting when dealing with threaded display
428             :param ascent: performs an ascended search; will add to fetched msgs
429                            all their parents until root_ids
430             :param root_ids: for ascent search
431             :param root_ids: root_ids when performing an ascended search
432         """
433         if context is None:
434             context = {}
435         msg_obj = self.pool.get('mail.message')
436         msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', ids), ('model', '=', self._name)] + domain,
437             limit=limit, offset=offset, context=context)
438         if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
439         return msg_ids
440
441     def message_load(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
442         """ OpenChatter feature: return thread messages
443         """
444         msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
445         msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
446         
447         # Set as read
448         self.message_check_and_set_read(cr, uid, ids, context=context)
449         
450         """ Retrieve all attachments names """
451         map_id_to_name = {}
452         
453         for msg in msgs:
454             for attach_id in msg["attachment_ids"]:
455                 map_id_to_name[attach_id] = '' # use empty string as a placeholder
456         
457         ids = map_id_to_name.keys()
458         names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
459         
460         # convert the list of tuples into a dictionnary
461         for name in names: 
462             map_id_to_name[name[0]] = name[1]
463         
464         # give corresponding ids and names to each message
465         for msg in msgs:
466             msg["attachments"] = []
467             
468             for attach_id in msg["attachment_ids"]:
469                 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
470         
471         """ Sort and return messages """
472         msgs = sorted(msgs, key=lambda d: (-d['id']))
473         return msgs
474
475     def get_pushed_messages(self, cr, uid, ids, limit=100, offset=0, msg_search_domain=[], ascent=False, root_ids=[], context=None):
476         """ OpenChatter: wall: get messages to display (=pushed notifications)
477             :param domain: domain to add to the search; especially child_of
478                            is interesting when dealing with threaded display
479             :param ascent: performs an ascended search; will add to fetched msgs
480                            all their parents until root_ids
481             :param root_ids: for ascent search
482             :return list of mail.messages sorted by date
483         """
484         if context is None: context = {}
485         notification_obj = self.pool.get('mail.notification')
486         msg_obj = self.pool.get('mail.message')
487         # update message search
488         for arg in msg_search_domain:
489             if isinstance(arg, (tuple, list)):
490                 arg[0] = 'message_id.' + arg[0]
491         # compose final domain
492         domain = [('user_id', '=', uid)] + msg_search_domain
493         # get notifications
494         notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
495         notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
496         msg_ids = [notification.message_id.id for notification in notifications]
497         # get messages
498         msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
499         if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
500         msgs = msg_obj.read(cr, uid, msg_ids, context=context)
501         return msgs
502
503     #------------------------------------------------------
504     # Mail gateway
505     #------------------------------------------------------
506     # message_process will call either message_new or message_update.
507
508     def message_process(self, cr, uid, model, message, custom_values=None,
509                         save_original=False, strip_attachments=False,
510                         context=None):
511         """Process an incoming RFC2822 email message related to the
512            given thread model, relying on ``mail.message.parse()``
513            for the parsing operation, and then calling ``message_new``
514            (if the thread record did not exist) or ``message_update``
515            (if it did), then calling ``message_forward`` to automatically
516            notify other people that should receive this message.
517
518            :param string model: the thread model for which a new message
519                                 must be processed
520            :param message: source of the RFC2822 mail
521            :type message: string or xmlrpclib.Binary
522            :type dict custom_values: optional dictionary of field values
523                                     to pass to ``message_new`` if a new
524                                     record needs to be created. Ignored
525                                     if the thread record already exists.
526            :param bool save_original: whether to keep a copy of the original
527                email source attached to the message after it is imported.
528            :param bool strip_attachments: whether to strip all attachments
529                before processing the message, in order to save some space.
530         """
531         # extract message bytes - we are forced to pass the message as binary because
532         # we don't know its encoding until we parse its headers and hence can't
533         # convert it to utf-8 for transport between the mailgate script and here.
534         if isinstance(message, xmlrpclib.Binary):
535             message = str(message.data)
536
537         model_pool = self.pool.get(model)
538         if self._name != model:
539             if context is None: context = {}
540             context.update({'thread_model': model})
541
542         mail_message = self.pool.get('mail.message')
543         res_id = False
544
545         # Parse Message
546         # Warning: message_from_string doesn't always work correctly on unicode,
547         # we must use utf-8 strings here :-(
548         if isinstance(message, unicode):
549             message = message.encode('utf-8')
550         msg_txt = email.message_from_string(message)
551         msg = mail_message.parse_message(msg_txt, save_original=save_original)
552
553         if strip_attachments and 'attachments' in msg:
554             del msg['attachments']
555
556         # Create New Record into particular model
557         def create_record(msg):
558             if hasattr(model_pool, 'message_new'):
559                 return model_pool.message_new(cr, uid, msg,
560                                               custom_values,
561                                               context=context)
562         res_id = False
563         if msg.get('references') or msg.get('in-reply-to'):
564             references = msg.get('references') or msg.get('in-reply-to')
565             if '\r\n' in references:
566                 references = references.split('\r\n')
567             else:
568                 references = references.split(' ')
569             for ref in references:
570                 ref = ref.strip()
571                 res_id = tools.reference_re.search(ref)
572                 if res_id:
573                     res_id = res_id.group(1)
574                 else:
575                     res_id = tools.res_re.search(msg['subject'])
576                     if res_id:
577                         res_id = res_id.group(1)
578                 if res_id:
579                     res_id = res_id
580                     if model_pool.exists(cr, uid, res_id):
581                         if hasattr(model_pool, 'message_update'):
582                             model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
583                     else:
584                         # referenced thread was not found, we'll have to create a new one
585                         res_id = False
586         if not res_id:
587             res_id = create_record(msg)
588         # To forward the email to other followers
589         self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
590         return res_id
591
592     def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
593         """Called by ``message_process`` when a new message is received
594            for a given thread model, if the message did not belong to
595            an existing thread.
596            The default behavior is to create a new record of the corresponding
597            model (based on some very basic info extracted from the message),
598            then attach the message to the newly created record
599            (by calling ``message_append_dict``).
600            Additional behavior may be implemented by overriding this method.
601
602            :param dict msg_dict: a map containing the email details and
603                                  attachments. See ``message_process`` and
604                                 ``mail.message.parse`` for details.
605            :param dict custom_values: optional dictionary of additional
606                                       field values to pass to create()
607                                       when creating the new thread record.
608                                       Be careful, these values may override
609                                       any other values coming from the message.
610            :param dict context: if a ``thread_model`` value is present
611                                 in the context, its value will be used
612                                 to determine the model of the record
613                                 to create (instead of the current model).
614            :rtype: int
615            :return: the id of the newly created thread object
616         """
617         if context is None:
618             context = {}
619         model = context.get('thread_model') or self._name
620         model_pool = self.pool.get(model)
621         fields = model_pool.fields_get(cr, uid, context=context)
622         data = model_pool.default_get(cr, uid, fields, context=context)
623         if 'name' in fields and not data.get('name'):
624             data['name'] = msg_dict.get('from', '')
625         if custom_values and isinstance(custom_values, dict):
626             data.update(custom_values)
627         res_id = model_pool.create(cr, uid, data, context=context)
628         self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
629         return res_id
630
631     def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
632         """ Called by ``message_process`` when a new message is received
633             for an existing thread. The default behavior is to create a
634             new mail.message in the given thread (by calling
635             ``message_append_dict``)
636             Additional behavior may be implemented by overriding this
637             method.
638
639             :param dict msg_dict: a map containing the email details and
640                                 attachments. See ``message_process`` and
641                                 ``mail.message.parse()`` for details.
642             :param dict vals: a dict containing values to update records
643                               given their ids; if the dict is None or is
644                               void, no write operation is performed.
645             :param dict context: if a ``thread_model`` value is present
646                                 in the context, its value will be used
647                                 to determine the model of the thread to
648                                 update (instead of the current model).
649         """
650         if update_vals:
651             self.write(cr, uid, ids, update_vals, context=context)
652         return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
653
654     def message_thread_followers(self, cr, uid, ids, context=None):
655         """Returns a list of email addresses of the people following
656            this thread, including the sender of each mail, and the
657            people who were in CC of the messages, if any.
658         """
659         res = {}
660         if isinstance(ids, (str, int, long)):
661             ids = [long(ids)]
662         for thread in self.browse(cr, uid, ids, context=context):
663             l = set()
664             for message in thread.message_ids:
665                 l.add((message.user_id and message.user_id.user_email) or '')
666                 l.add(message.email_from or '')
667                 l.add(message.email_cc or '')
668             res[thread.id] = filter(None, l)
669         return res
670
671     def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
672         """Sends an email to all people following the given threads.
673            The emails are forwarded immediately, not queued for sending,
674            and not archived.
675
676         :param str model: thread model
677         :param list thread_ids: ids of the thread records
678         :param msg: email.message.Message object to forward
679         :param email_error: optional email address to notify in case
680                             of any delivery error during the forward.
681         :return: True
682         """
683         model_pool = self.pool.get(model)
684         smtp_server_obj = self.pool.get('ir.mail_server')
685         mail_message = self.pool.get('mail.message')
686         for res in model_pool.browse(cr, uid, thread_ids, context=context):
687             if hasattr(model_pool, 'message_thread_followers'):
688                 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
689             else:
690                 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
691             message_followers_emails = to_email(','.join(filter(None, followers)))
692             message_recipients = to_email(','.join(filter(None,
693                                                                        [decode(msg['from']),
694                                                                         decode(msg['to']),
695                                                                         decode(msg['cc'])])))
696             forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
697             if forward_to:
698                 # TODO: we need an interface for this for all types of objects, not just leads
699                 if hasattr(res, 'section_id'):
700                     del msg['reply-to']
701                     msg['reply-to'] = res.section_id.reply_to
702
703                 smtp_from, = to_email(msg['from'])
704                 msg['from'] = smtp_from
705                 msg['to'] =  ", ".join(forward_to)
706                 msg['message-id'] = tools.generate_tracking_message_id(res.id)
707                 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
708                     subj = msg['subject']
709                     del msg['subject'], msg['to'], msg['cc'], msg['bcc']
710                     msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
711                     msg['to'] = email_error
712                     smtp_server_obj.send_email(cr, uid, msg)
713         return True
714
715     def message_partner_by_email(self, cr, uid, email, context=None):
716         """Attempts to return the id of a partner address matching
717            the given ``email``, and the corresponding partner id.
718            Can be used by classes using the ``mail.thread`` mixin
719            to lookup the partner and use it in their implementation
720            of ``message_new`` to link the new record with a
721            corresponding partner.
722            The keys used in the returned dict are meant to map
723            to usual names for relationships towards a partner
724            and one of its addresses.
725
726            :param email: email address for which a partner
727                          should be searched for.
728            :rtype: dict
729            :return: a map of the following form::
730
731                       { 'partner_address_id': id or False,
732                         'partner_id': pid or False }
733         """
734         partner_pool = self.pool.get('res.partner')
735         res = {'partner_id': False}
736         if email:
737             email = to_email(email)[0]
738             contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
739             if contact_ids:
740                 contact = partner_pool.browse(cr, uid, contact_ids[0])
741                 res['partner_id'] = contact.id
742         return res
743
744     # for backwards-compatibility with old scripts
745     process_email = message_process
746
747     #------------------------------------------------------
748     # Note specific
749     #------------------------------------------------------
750
751     def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
752         if context is None:
753             context = {}
754         notification_obj = self.pool.get('mail.notification')
755         # write message
756         msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, subtype=subtype, context=context)
757         # escape if in install mode or note writing was not successfull
758         if 'install_mode' in context:
759             return True
760         if not isinstance(msg_ids, (list)):
761             return True
762         # get already existing notigications
763         notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
764         already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
765         # get base.group_user group
766         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
767         group_id = res and res[1] or False
768         if not group_id: return True
769         group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
770         for user in group.users:
771             if user.id in already_pushed_user_ids: continue
772             for msg_id in msg_ids:
773                 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
774         return True
775
776     def log(self, cr, uid, id, message, secondary=False, context=None):
777         _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
778         self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
779
780     def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
781         if type in ['notification', 'reply']:
782             subject = None
783         if subtype == 'html':
784             body_html = body
785             body_text = body
786         else:
787             body_html = body
788             body_text = body
789         return self.message_append(cr, uid, ids, subject, body_html=body_html, body_text=body_text, parent_id=parent_id, type=type, subtype=subtype, context=context)
790
791     #------------------------------------------------------
792     # Subscription mechanism
793     #------------------------------------------------------
794
795     def message_get_subscribers(self, cr, uid, ids, context=None):
796         """ Returns the current document followers. Basically this method
797             checks in mail.subscription for entries with matching res_model,
798             res_id.
799         
800             :param get_ids: if set to True, return the ids of users; if set
801                             to False, returns the result of a read in res.users
802         """
803         subscr_obj = self.pool.get('mail.subscription')
804         subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
805         return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
806
807     def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
808         """ Returns the current document followers as a read result. Used
809             mainly for Chatter having only one method to call to have
810             details about users.
811         """
812         user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
813         return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
814
815     def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
816         """ Check if uid or user_id (if set) is a subscriber to the current
817             document.
818             
819             :param user_id: if set, check is done on user_id; if not set
820                             check is done on uid
821         """
822         sub_user_id = uid if user_id is None else user_id
823         if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
824             return True
825         return False
826
827     def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
828         """ Subscribe the user (or user_ids) to the current document.
829             
830             :param user_ids: a list of user_ids; if not set, subscribe
831                              uid instead
832         """
833         subscription_obj = self.pool.get('mail.subscription')
834         to_subscribe_uids = [uid] if user_ids is None else user_ids
835         create_ids = []
836         for id in ids:
837             already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
838             for user_id in to_subscribe_uids:
839                 if user_id in already_subscribed_user_ids: continue
840                 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
841         return create_ids
842
843     def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
844         """ Unsubscribe the user (or user_ids) from the current document.
845             
846             :param user_ids: a list of user_ids; if not set, subscribe
847                              uid instead
848         """
849         # Trying to unsubscribe somebody not in subscribers: returns False
850         # if special management is needed; allows to know that an automatically
851         # subscribed user tries to unsubscribe and allows to warn him
852         mail_thread_model = self.pool.get('mail.thread')
853         if not user_ids and not uid in mail_thread_model.message_get_subscribers(cr, uid, ids, context=context):
854             return False
855         subscription_obj = self.pool.get('mail.subscription')
856         to_unsubscribe_uids = [uid] if user_ids is None else user_ids
857         to_delete_sub_ids = subscription_obj.search(cr, uid,
858                         ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
859         return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
860
861     #------------------------------------------------------
862     # Notification API
863     #------------------------------------------------------
864
865     def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
866         notif_obj = self.pool.get('mail.notification')
867         msg_obj = self.pool.get('mail.message')
868         if remove_childs:
869             notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
870         else:
871             notif_msg_ids = msg_ids
872         to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
873         return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
874
875     #------------------------------------------------------
876     # Thread_state
877     #------------------------------------------------------
878
879     def message_create_set_unread(self, cr, uid, ids, context=None):
880         """ When creating a new message, set as unread if uid is not the
881             object responsible. """
882         for obj in self.browse(cr, uid, ids, context=context):
883             if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
884                 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
885
886     def message_check_and_set_unread(self, cr, uid, ids, context=None):
887         """ Set unread if uid is the object responsible or if the object has
888             no responsible. """
889         for obj in self.browse(cr, uid, ids, context=context):
890             if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
891                 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
892
893     def message_mark_as_unread(self, cr, uid, ids, context=None):
894         """ Set as unread. """
895         return self.write(cr, uid, ids, {'message_state': False}, context=context)
896
897     def message_check_and_set_read(self, cr, uid, ids, context=None):
898         """ Set read if uid is the object responsible. """
899         for obj in self.browse(cr, uid, ids, context=context):
900             if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
901                 self.message_mark_as_read(cr, uid, [obj.id], context=context)
902     
903     def message_mark_as_read(self, cr, uid, ids, context=None):
904         """ Set as read. """
905         return self.write(cr, uid, ids, {'message_state': True}, context=context)
906
907
908 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: