a0da4ecc9bc1001d320f8a3f43118d523bbad1a4
[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_search(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 messages and 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         notification_obj = self.pool.get('mail.notification')
154         body = vals.get('body_html', '') if vals.get('content_subtype') == 'html' else vals.get('body_text', '')
155         
156         # automatically subscribe the writer of the message
157         if vals['user_id']:
158             self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
159         
160         # create message
161         msg_id = message_obj.create(cr, uid, vals, context=context)
162         
163         # Set as unread if writer is not the document responsible
164         self.message_create_set_unread(cr, uid, [thread_id], context=context)
165         
166         # special: if install mode, do not push demo data
167         if context.get('install_mode', False):
168             return msg_id
169         
170         # get users that will get a notification pushed
171         user_to_push_ids = self.message_get_user_ids_to_notify(cr, uid, [thread_id], vals, context=context)
172         for id in user_to_push_ids:
173             notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
174         
175         # create the email to send
176         email_id = self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
177         
178         return msg_id
179     
180     def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
181         subscription_obj = self.pool.get('mail.subscription')
182         hide_obj = self.pool.get('mail.subscription.hide')
183         # get body
184         body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
185         
186         # get subscribers
187         user_sub_ids = self.message_get_subscribers(cr, uid, thread_ids, context=context)
188         
189         # get hiden subscriptions
190         subscription_ids = subscription_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', thread_ids), ('user_id', 'in', user_sub_ids)], context=context)
191         hide_ids = hide_obj.search(cr, uid, [('subscription_id', 'in', subscription_ids), ('subtype', '=', new_msg_vals.get('subtype'))], context=context)
192         if hide_ids:
193             hiden_subscriptions = hide_obj.browse(cr, uid, hide_ids, context=context)
194             hiden_user_ids = [hiden_subscription.subscription_id.user_id.id for hiden_subscription in hiden_subscriptions]
195             notif_user_ids = [user_id for user_id in user_sub_ids if not user_id in hiden_user_ids]
196         else:
197             notif_user_ids = user_sub_ids
198         
199         # add users requested via parsing message (@login)
200         notif_user_ids += self.message_parse_users(cr, uid, body, context=context)
201         
202         # add users requested to perform an action (need_action mechanism)
203         if hasattr(self, 'get_needaction_user_ids'):
204             user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context)
205             for id, user_ids in user_ids_dict.iteritems():
206                 notif_user_ids += user_ids
207         
208         # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
209         if new_msg_vals.get('parent_id'):
210             notif_obj = self.pool.get('mail.notification')
211             parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
212             parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
213             notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
214
215         # remove duplicate entries
216         notif_user_ids = list(set(notif_user_ids))
217         return notif_user_ids
218
219     def message_parse_users(self, cr, uid, string, context=None):
220         """Parse message content
221            - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
222              this supports login that are emails (such as @raoul@grobedon.net)
223         """
224         regex = re.compile('(^|\s)@((\w|@|\.)*)')
225         login_lst = [item[1] for item in regex.findall(string)]
226         if not login_lst: return []
227         user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
228         return user_ids
229
230     #------------------------------------------------------
231     # Generic message api
232     #------------------------------------------------------
233
234     def message_capable_models(self, cr, uid, context=None):
235         ret_dict = {}
236         for model_name in self.pool.obj_list():
237             model = self.pool.get(model_name)
238             if 'mail.thread' in getattr(model, '_inherit', []):
239                 ret_dict[model_name] = model._description
240         return ret_dict
241
242     def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
243                         type='email', subtype=None, email_date=None, parent_id=False,
244                         content_subtype='plain', state=None,
245                         partner_ids=None, email_from=False, email_to=False,
246                         email_cc=None, email_bcc=None, reply_to=None,
247                         headers=None, message_id=False, references=None,
248                         attachments=None, original=None, context=None):
249         """ Creates a new mail.message through message_create. The new message
250             is attached to the current mail.thread, containing all the details 
251             passed as parameters. All attachments will be attached to the 
252             thread record as well as to the actual message.
253            
254             This method calls message_create that will handle management of
255             subscription and notifications, and effectively create the message.
256            
257             If ``email_from`` is not set or ``type`` not set as 'email',
258             a note message is created (comment or system notification), 
259             without the usual envelope attributes (sender, recipients, etc.).
260
261             :param threads: list of thread ids, or list of browse_records
262                 representing threads to which a new message should be attached
263             :param subject: subject of the message, or description of the event;
264                 this is totally optional as subjects are not important except
265                 for specific messages (blog post, job offers) or for emails
266             :param body_text: plaintext contents of the mail or log message
267             :param body_html: html contents of the mail or log message
268             :param type: type of message: 'email', 'comment', 'notification';
269                 email by default
270             :param subtype: subtype of message, such as 'create' or 'cancel'
271             :param email_date: email date string if different from now, in
272                 server timezone
273             :param parent_id: id of the parent message (threaded messaging model)
274             :param content_subtype: optional content_subtype of message: 'plain'
275                 or 'html', corresponding to the main body contents (body_text or
276                 body_html).
277             :param state: state of message
278             :param partner_ids: destination partners of the message, in addition
279                 to the now fully optional email_to; this method is supposed to
280                 received a list of ids is not None. The specific many2many
281                 instruction will be generated by this method.
282             :param email_from: Email From / Sender address if any
283             :param email_to: Email-To / Recipient address
284             :param email_cc: Comma-Separated list of Carbon Copy Emails To
285                 addresses if any
286             :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To
287                 addresses if any
288             :param reply_to: reply_to header
289             :param headers: mail headers to store
290             :param message_id: optional email identifier
291             :param references: optional email references
292             :param dict attachments: map of attachment filenames to binary
293                 contents, if any.
294             :param str original: optional full source of the RFC2822 email, for
295                 reference
296             :param dict context: if a ``thread_model`` value is present in the
297                 context, its value will be used to determine the model of the
298                 thread to update (instead of the current model).
299         """
300         if context is None:
301             context = {}
302         if attachments is None:
303             attachments = {}
304
305         if email_date:
306             edate = parsedate(email_date)
307             if edate is not None:
308                 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
309
310         if all(isinstance(thread_id, (int, long)) for thread_id in threads):
311             model = context.get('thread_model') or self._name
312             model_pool = self.pool.get(model)
313             threads = model_pool.browse(cr, uid, threads, context=context)
314
315         ir_attachment = self.pool.get('ir.attachment')
316         mail_message = self.pool.get('mail.message')
317
318         new_msg_ids = []
319         for thread in threads:
320             to_attach = []
321             for attachment in attachments:
322                 fname, fcontent = attachment
323                 if isinstance(fcontent, unicode):
324                     fcontent = fcontent.encode('utf-8')
325                 data_attach = {
326                     'name': fname,
327                     'datas': base64.b64encode(str(fcontent)),
328                     'datas_fname': fname,
329                     'description': _('Mail attachment'),
330                     'res_model': thread._name,
331                     'res_id': thread.id,
332                 }
333                 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
334             # find related partner: partner_id column in thread object, or self is res.partner model
335             partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
336             if not partner_id and thread._name == 'res.partner':
337                 partner_id = thread.id
338             # destination partners
339             if partner_ids is None:
340                 partner_ids = []
341             mail_partner_ids = [6, 0, partner_ids]
342
343             data = {
344                 'subject': subject,
345                 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
346                 'body_html': body_html or '',
347                 'parent_id': parent_id,
348                 'date': email_date or fields.datetime.now(),
349                 'type': type,
350                 'subtype': subtype,
351                 'content_subtype': content_subtype,
352                 'state': state,
353                 'message_id': message_id,
354                 'partner_ids': mail_partner_ids,
355                 'attachment_ids': [(6, 0, to_attach)],
356                 'user_id': uid,
357                 'model' : thread._name,
358                 'res_id': thread.id,
359                 'partner_id': partner_id,
360             }
361
362             if email_from or type == 'email':
363                 for param in (email_to, email_cc, email_bcc):
364                     if isinstance(param, list):
365                         param = ", ".join(param)
366                 data.update({
367                     'email_to': email_to,
368                     'email_from': email_from or \
369                         (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
370                     'email_cc': email_cc,
371                     'email_bcc': email_bcc,
372                     'references': references,
373                     'headers': headers,
374                     'reply_to': reply_to,
375                     'original': original, })
376
377             new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
378         return new_msg_ids
379
380     def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
381         """Creates a new mail.message attached to the given threads (``ids``),
382            with the contents of ``msg_dict``, by calling ``message_append``
383            with the mail details. All attachments in msg_dict will be
384            attached to the object record as well as to the actual
385            mail message.
386
387            :param dict msg_dict: a map containing the email details and
388                                  attachments. See ``message_process()`` and
389                                 ``mail.message.parse()`` for details on
390                                 the dict structure.
391            :param dict context: if a ``thread_model`` value is present
392                                 in the context, its value will be used
393                                 to determine the model of the thread to
394                                 update (instead of the current model).
395         """
396         return self.message_append(cr, uid, ids,
397                             subject = msg_dict.get('subject'),
398                             body_text = msg_dict.get('body_text'),
399                             body_html= msg_dict.get('body_html'),
400                             parent_id = msg_dict.get('parent_id', False),
401                             type = msg_dict.get('type', 'email'),
402                             subtype = msg_dict.get('subtype'),
403                             content_subtype = msg_dict.get('content_subtype'),
404                             state = msg_dict.get('state'),
405                             partner_ids = msg_dict.get('partner_ids'),
406                             email_from = msg_dict.get('from', msg_dict.get('email_from')),
407                             email_to = msg_dict.get('to', msg_dict.get('email_to')),
408                             email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
409                             email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
410                             reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
411                             email_date = msg_dict.get('date'),
412                             message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
413                             references = msg_dict.get('references')\
414                                       or msg_dict.get('in-reply-to'),
415                             attachments = msg_dict.get('attachments'),
416                             headers = msg_dict.get('headers'),
417                             original = msg_dict.get('original'),
418                             context = context)
419
420     #------------------------------------------------------
421     # Message loading
422     #------------------------------------------------------
423
424     def _message_search_ancestor_ids(self, cr, uid, ids, child_ids, ancestor_ids, context=None):
425         """ Given message child_ids ids, find their ancestors until ancestor_ids
426             using their parent_id relationship.
427
428             :param child_ids: the first nodes of the search
429             :param ancestor_ids: list of ancestors. When the search reach an
430                                  ancestor, it stops.
431         """
432         def _get_parent_ids(message_list, ancestor_ids, child_ids):
433             """ Tool function: return the list of parent_ids of messages
434                 contained in message_list. Parents that are in ancestor_ids
435                 or in child_ids are not returned. """
436             return [message['parent_id'][0] for message in message_list
437                         if message['parent_id']
438                         and message['parent_id'][0] not in ancestor_ids
439                         and message['parent_id'][0] not in child_ids
440                     ]
441
442         message_obj = self.pool.get('mail.message')
443         messages_temp = message_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
444         parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
445         child_ids += parent_ids
446         cur_iter = 0; max_iter = 100; # avoid infinite loop
447         while (parent_ids and (cur_iter < max_iter)):
448             cur_iter += 1
449             messages_temp = message_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
450             parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
451             child_ids += parent_ids
452         if (cur_iter > max_iter):
453             _logger.warning("Possible infinite loop in _message_search_ancestor_ids. "\
454                 "Note that this algorithm is intended to check for cycle in "\
455                 "message graph, leading to a curious error. Have fun.")
456         return child_ids
457
458     def message_search_get_domain(self, cr, uid, ids, context=None):
459         """ OpenChatter feature: get the domain to search the messages related
460             to a document. mail.thread defines the default behavior as
461             being messages with model = self._name, id in ids.
462             This method should be overridden if a model has to implement a
463             particular behavior.
464         """
465         return ['&', ('res_id', 'in', ids), ('model', '=', self._name)]
466
467     def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None, 
468                         limit=100, offset=0, domain=None, count=False, context=None):
469         """ OpenChatter feature: return thread messages ids according to the
470             search domain given by ``message_search_get_domain``.
471             
472             It is possible to add in the search the parent of messages by
473             setting the fetch_ancestors flag to True. In that case, using
474             the parent_id relationship, the method returns the id list according
475             to the search domain, but then calls ``_message_search_ancestor_ids``
476             that will add to the list the ancestors ids. The search is limited
477             to parent messages having an id in ancestor_ids or having
478             parent_id set to False.
479             
480             If ``count==True``, the number of ids is returned instead of the
481             id list. The count is done by hand instead of passing it as an 
482             argument to the search call because we might want to perform
483             a research including parent messages until some ancestor_ids.
484             
485             :param fetch_ancestors: performs an ascended search; will add 
486                                     to fetched msgs all their parents until
487                                     ancestor_ids
488             :param ancestor_ids: used when fetching ancestors
489             :param domain: domain to add to the search; especially child_of
490                            is interesting when dealing with threaded display.
491                            Note that the added domain is anded with the 
492                            default domain.
493             :param limit, offset, count, context: as usual
494         """
495         search_domain = self.message_search_get_domain(cr, uid, ids, context=context)
496         if domain:
497             search_domain += domain
498         message_obj = self.pool.get('mail.message')
499         message_res = message_obj.search(cr, uid, search_domain, limit=limit, offset=offset, count=count, context=context)
500         if not count and fetch_ancestors:
501             message_res += self._message_search_ancestor_ids(cr, uid, ids, message_res, ancestor_ids, context=context) 
502         return message_res
503
504     def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None, 
505                         limit=100, offset=0, domain=None, context=None):
506         """ OpenChatter feature: read the messages related to some threads.
507             This method is used mainly the Chatter widget, to directly have
508             read result instead of searching then reading.
509
510             Please see message_search for more information about the parameters.
511         """
512         message_ids = self.message_search(cr, uid, ids, fetch_ancestors, ancestor_ids,
513             limit, offset, domain, context=context)
514         messages = self.pool.get('mail.message').read(cr, uid, message_ids, context=context)
515         
516         """ Retrieve all attachments names """
517         map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
518         print map_id_to_name
519         map_id_to_name = {}
520         for msg in messages:
521             for attach_id in msg["attachment_ids"]:
522                 map_id_to_name[attach_id] = '' # use empty string as a placeholder
523         print map_id_to_name
524
525         ids = map_id_to_name.keys()
526         names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
527         
528         # convert the list of tuples into a dictionnary
529         for name in names: 
530             map_id_to_name[name[0]] = name[1]
531         
532         # give corresponding ids and names to each message
533         for msg in messages:
534             msg["attachments"] = []
535             
536             for attach_id in msg["attachment_ids"]:
537                 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
538         
539         # Set the threads as read
540         self.message_check_and_set_read(cr, uid, ids, context=context)
541         # Sort and return the messages
542         messages = sorted(messages, key=lambda d: (-d['id']))
543         return messages
544
545     def message_get_pushed_messages(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
546                             limit=100, offset=0, msg_search_domain=[], context=None):
547         """ OpenChatter: wall: get the pushed notifications and used them
548             to fetch messages to display on the wall.
549             
550             :param fetch_ancestors: performs an ascended search; will add
551                                     to fetched msgs all their parents until
552                                     ancestor_ids
553             :param ancestor_ids: used when fetching ancestors
554             :param domain: domain to add to the search; especially child_of
555                            is interesting when dealing with threaded display
556             :param ascent: performs an ascended search; will add to fetched msgs
557                            all their parents until root_ids
558             :param root_ids: for ascent search
559             :return: list of mail.messages sorted by date
560         """
561         notification_obj = self.pool.get('mail.notification')
562         msg_obj = self.pool.get('mail.message')
563         # update message search
564         for arg in msg_search_domain:
565             if isinstance(arg, (tuple, list)):
566                 arg[0] = 'message_id.' + arg[0]
567         # compose final domain
568         domain = [('user_id', '=', uid)] + msg_search_domain
569         # get notifications
570         notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
571         notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
572         msg_ids = [notification.message_id.id for notification in notifications]
573         # get messages
574         msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
575         if (fetch_ancestors): msg_ids = self._message_search_ancestor_ids(cr, uid, ids, msg_ids, ancestor_ids, context=context)
576         msgs = msg_obj.read(cr, uid, msg_ids, context=context)
577         return msgs
578
579     #------------------------------------------------------
580     # Mail gateway
581     #------------------------------------------------------
582     # message_process will call either message_new or message_update.
583
584     def message_process(self, cr, uid, model, message, custom_values=None,
585                         save_original=False, strip_attachments=False,
586                         context=None):
587         """Process an incoming RFC2822 email message related to the
588            given thread model, relying on ``mail.message.parse()``
589            for the parsing operation, and then calling ``message_new``
590            (if the thread record did not exist) or ``message_update``
591            (if it did), then calling ``message_forward`` to automatically
592            notify other people that should receive this message.
593
594            :param string model: the thread model for which a new message
595                                 must be processed
596            :param message: source of the RFC2822 mail
597            :type message: string or xmlrpclib.Binary
598            :type dict custom_values: optional dictionary of field values
599                                     to pass to ``message_new`` if a new
600                                     record needs to be created. Ignored
601                                     if the thread record already exists.
602            :param bool save_original: whether to keep a copy of the original
603                 email source attached to the message after it is imported.
604            :param bool strip_attachments: whether to strip all attachments
605                 before processing the message, in order to save some space.
606         """
607         # extract message bytes - we are forced to pass the message as binary because
608         # we don't know its encoding until we parse its headers and hence can't
609         # convert it to utf-8 for transport between the mailgate script and here.
610         if isinstance(message, xmlrpclib.Binary):
611             message = str(message.data)
612
613         model_pool = self.pool.get(model)
614         if self._name != model:
615             if context is None: context = {}
616             context.update({'thread_model': model})
617
618         mail_message = self.pool.get('mail.message')
619
620         # Parse Message
621         # Warning: message_from_string doesn't always work correctly on unicode,
622         # we must use utf-8 strings here :-(
623         if isinstance(message, unicode):
624             message = message.encode('utf-8')
625         msg_txt = email.message_from_string(message)
626         msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context)
627
628         # update state
629         msg['state'] = 'received'
630         
631         if strip_attachments and 'attachments' in msg:
632             del msg['attachments']
633
634         # Create New Record into particular model
635         def create_record(msg):
636             if hasattr(model_pool, 'message_new'):
637                 return model_pool.message_new(cr, uid, msg,
638                                               custom_values,
639                                               context=context)
640         res_id = False
641         if msg.get('references') or msg.get('in-reply-to'):
642             references = msg.get('references') or msg.get('in-reply-to')
643             if '\r\n' in references:
644                 references = references.split('\r\n')
645             else:
646                 references = references.split(' ')
647             for ref in references:
648                 ref = ref.strip()
649                 res_id = tools.reference_re.search(ref)
650                 if res_id:
651                     res_id = res_id.group(1)
652                 else:
653                     res_id = tools.res_re.search(msg['subject'])
654                     if res_id:
655                         res_id = res_id.group(1)
656                 if res_id:
657                     res_id = res_id
658                     if model_pool.exists(cr, uid, res_id):
659                         if hasattr(model_pool, 'message_update'):
660                             model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
661                     else:
662                         # referenced thread was not found, we'll have to create a new one
663                         res_id = False
664         if not res_id:
665             res_id = create_record(msg)
666         # To forward the email to other followers
667         self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
668         # Set as Unread
669         model_pool.message_mark_as_unread(cr, uid, [res_id], context=context)
670         return res_id
671
672     def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
673         """Called by ``message_process`` when a new message is received
674            for a given thread model, if the message did not belong to
675            an existing thread.
676            The default behavior is to create a new record of the corresponding
677            model (based on some very basic info extracted from the message),
678            then attach the message to the newly created record
679            (by calling ``message_append_dict``).
680            Additional behavior may be implemented by overriding this method.
681
682            :param dict msg_dict: a map containing the email details and
683                                  attachments. See ``message_process`` and
684                                 ``mail.message.parse`` for details.
685            :param dict custom_values: optional dictionary of additional
686                                       field values to pass to create()
687                                       when creating the new thread record.
688                                       Be careful, these values may override
689                                       any other values coming from the message.
690            :param dict context: if a ``thread_model`` value is present
691                                 in the context, its value will be used
692                                 to determine the model of the record
693                                 to create (instead of the current model).
694            :rtype: int
695            :return: the id of the newly created thread object
696         """
697         if context is None:
698             context = {}
699         model = context.get('thread_model') or self._name
700         model_pool = self.pool.get(model)
701         fields = model_pool.fields_get(cr, uid, context=context)
702         data = model_pool.default_get(cr, uid, fields, context=context)
703         if 'name' in fields and not data.get('name'):
704             data['name'] = msg_dict.get('from', '')
705         if custom_values and isinstance(custom_values, dict):
706             data.update(custom_values)
707         res_id = model_pool.create(cr, uid, data, context=context)
708         self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
709         return res_id
710
711     def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
712         """ Called by ``message_process`` when a new message is received
713             for an existing thread. The default behavior is to create a
714             new mail.message in the given thread (by calling
715             ``message_append_dict``)
716             Additional behavior may be implemented by overriding this
717             method.
718
719             :param dict msg_dict: a map containing the email details and
720                                 attachments. See ``message_process`` and
721                                 ``mail.message.parse()`` for details.
722             :param dict vals: a dict containing values to update records
723                               given their ids; if the dict is None or is
724                               void, no write operation is performed.
725             :param dict context: if a ``thread_model`` value is present
726                                 in the context, its value will be used
727                                 to determine the model of the thread to
728                                 update (instead of the current model).
729         """
730         if update_vals:
731             self.write(cr, uid, ids, update_vals, context=context)
732         return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
733
734     def message_thread_followers(self, cr, uid, ids, context=None):
735         """ Returns a list of email addresses of the people following
736             this thread, including the sender of each mail, and the
737             people who were in CC of the messages, if any.
738         """
739         res = {}
740         if isinstance(ids, (str, int, long)):
741             ids = [long(ids)]
742         for thread in self.browse(cr, uid, ids, context=context):
743             l = set()
744             for message in thread.message_ids:
745                 l.add((message.user_id and message.user_id.user_email) or '')
746                 l.add(message.email_from or '')
747                 l.add(message.email_cc or '')
748             res[thread.id] = filter(None, l)
749         return res
750
751     def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
752         """Sends an email to all people following the given threads.
753            The emails are forwarded immediately, not queued for sending,
754            and not archived.
755
756         :param str model: thread model
757         :param list thread_ids: ids of the thread records
758         :param msg: email.message.Message object to forward
759         :param email_error: optional email address to notify in case
760                             of any delivery error during the forward.
761         :return: True
762         """
763         model_pool = self.pool.get(model)
764         smtp_server_obj = self.pool.get('ir.mail_server')
765         mail_message = self.pool.get('mail.message')
766         for res in model_pool.browse(cr, uid, thread_ids, context=context):
767             if hasattr(model_pool, 'message_thread_followers'):
768                 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
769             else:
770                 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
771             message_followers_emails = to_email(','.join(filter(None, followers)))
772             message_recipients = to_email(','.join(filter(None,
773                                                                        [decode(msg['from']),
774                                                                         decode(msg['to']),
775                                                                         decode(msg['cc'])])))
776             forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
777             if forward_to:
778                 # TODO: we need an interface for this for all types of objects, not just leads
779                 if hasattr(res, 'section_id'):
780                     del msg['reply-to']
781                     msg['reply-to'] = res.section_id.reply_to
782
783                 smtp_from, = to_email(msg['from'])
784                 msg['from'] = smtp_from
785                 msg['to'] =  ", ".join(forward_to)
786                 msg['message-id'] = tools.generate_tracking_message_id(res.id)
787                 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
788                     subj = msg['subject']
789                     del msg['subject'], msg['to'], msg['cc'], msg['bcc']
790                     msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
791                     msg['to'] = email_error
792                     smtp_server_obj.send_email(cr, uid, msg)
793         return True
794
795     def message_partner_by_email(self, cr, uid, email, context=None):
796         """Attempts to return the id of a partner address matching
797            the given ``email``, and the corresponding partner id.
798            Can be used by classes using the ``mail.thread`` mixin
799            to lookup the partner and use it in their implementation
800            of ``message_new`` to link the new record with a
801            corresponding partner.
802            The keys used in the returned dict are meant to map
803            to usual names for relationships towards a partner
804            and one of its addresses.
805
806            :param email: email address for which a partner
807                          should be searched for.
808            :rtype: dict
809            :return: a map of the following form::
810
811                       { 'partner_address_id': id or False,
812                         'partner_id': pid or False }
813         """
814         partner_pool = self.pool.get('res.partner')
815         res = {'partner_id': False}
816         if email:
817             email = to_email(email)[0]
818             contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
819             if contact_ids:
820                 contact = partner_pool.browse(cr, uid, contact_ids[0])
821                 res['partner_id'] = contact.id
822         return res
823
824     # for backwards-compatibility with old scripts
825     process_email = message_process
826
827     #------------------------------------------------------
828     # Note specific
829     #------------------------------------------------------
830     
831     def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', content_subtype='html', context=None):
832         if context is None:
833             context = {}
834         notification_obj = self.pool.get('mail.notification')
835         # write message
836         msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, content_subtype=content_subtype, context=context)
837         # escape if in install mode or note writing was not successfull
838         if 'install_mode' in context:
839             return True
840         if not isinstance(msg_ids, (list)):
841             return True
842         # get already existing notigications
843         notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
844         already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
845         # get base.group_user group
846         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
847         group_id = res and res[1] or False
848         if not group_id: return True
849         group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
850         for user in group.users:
851             if user.id in already_pushed_user_ids: continue
852             for msg_id in msg_ids:
853                 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
854         return True
855
856     def log(self, cr, uid, id, message, secondary=False, context=None):
857         _logger.warning("log() is deprecated. As this module inherit from \
858                         mail.thread, the message will be managed by this \
859                         module instead of by the res.log mechanism. Please \
860                         use the mail.thread OpenChatter API instead of the \
861                         now deprecated res.log.")
862         self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
863
864     def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False,
865                             type='notification', content_subtype='html', subtype=None, context=None):
866         if type in ['notification', 'comment']:
867             subject = None
868         if content_subtype == 'html':
869             body_html = body
870             body_text = body
871         else:
872             body_html = body
873             body_text = body
874         return self.message_append(cr, uid, ids, subject, body_html, body_text,
875                                     type, subtype, parent_id=parent_id,
876                                     content_subtype=content_subtype, context=context)
877
878     #------------------------------------------------------
879     # Subscription mechanism
880     #------------------------------------------------------
881
882     def message_get_subscribers(self, cr, uid, ids, context=None):
883         """ Returns the current document followers. Basically this method
884             checks in mail.subscription for entries with matching res_model,
885             res_id.
886             This method can be overriden to add implicit subscribers, such
887             as project managers, by adding their user_id to the list of
888             ids returned by this method.
889         """
890         subscr_obj = self.pool.get('mail.subscription')
891         subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
892         return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
893
894     def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
895         """ Returns the current document followers as a read result. Used
896             mainly for Chatter having only one method to call to have
897             details about users.
898         """
899         user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
900         return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
901
902     def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
903         """ Check if uid or user_id (if set) is a subscriber to the current
904             document.
905             
906             :param user_id: if set, check is done on user_id; if not set
907                             check is done on uid
908         """
909         sub_user_id = uid if user_id is None else user_id
910         if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
911             return True
912         return False
913
914     def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
915         """ Subscribe the user (or user_ids) to the current document.
916             
917             :param user_ids: a list of user_ids; if not set, subscribe
918                              uid instead
919         """
920         subscription_obj = self.pool.get('mail.subscription')
921         to_subscribe_uids = [uid] if user_ids is None else user_ids
922         create_ids = []
923         for id in ids:
924             already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
925             for user_id in to_subscribe_uids:
926                 if user_id in already_subscribed_user_ids: continue
927                 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
928         return create_ids
929
930     def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
931         """ Unsubscribe the user (or user_ids) from the current document.
932             
933             :param user_ids: a list of user_ids; if not set, subscribe
934                              uid instead
935         """
936         # Trying to unsubscribe somebody not in subscribers: returns False
937         # if special management is needed; allows to know that an automatically
938         # subscribed user tries to unsubscribe and allows to warn him
939         to_unsubscribe_uids = [uid] if user_ids is None else user_ids
940         subscription_obj = self.pool.get('mail.subscription')
941         to_delete_sub_ids = subscription_obj.search(cr, uid,
942                         ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
943         if not to_delete_sub_ids:
944             return False
945         return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
946
947     def message_subscription_hide(self, cr, uid, ids, subtype=None, context=None):
948         """Hide notifications, allowing to tune the messages displayed on
949            the Wall.
950            :param subtype: a mail.message subtype. If None, it is means the
951                            user wants to hide all notifications coming from
952                            the document.
953         """
954         subscription_obj = self.pool.get('mail.subscription')
955         subscription_hide_obj = self.pool.get('mail.subscription.hide')
956         # find the related subscriptions
957         subscription_ids = subscription_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', '=', uid)], context=context)
958         # hide it
959         for subscription_id in subscription_ids:
960             subscription_hide_obj.create(cr, uid, {'subscription_id': subscription_id, 'subtype': subtype}, context=context)
961         return True
962
963     #------------------------------------------------------
964     # Notification API
965     #------------------------------------------------------
966
967     def message_create_notify_by_email(self, cr, uid, new_msg_values, user_to_notify_ids, context=None):
968         """ When creating a new message and pushing notifications, emails
969             must be send if users have chosen to receive notifications
970             by email via the notification_email_pref field.
971             
972             ``notification_email_pref`` can have 3 values :
973             - all: receive all notification by email (for example for shared
974               users)
975             - to_me: messages send directly to me (@login, messages on res.users)
976             - never: never receive notifications
977             Note that an user should never receive notifications for messages
978             he has created.
979             
980             :param new_msg_values: dictionary of message values, those that
981                                    are given to the create method
982             :param user_to_notify_ids: list of user_ids, user that will
983                                        receive a notification on their Wall
984         """
985         message_obj = self.pool.get('mail.message')
986         res_users_obj = self.pool.get('res.users')
987         body = new_msg_values.get('body_html', '') if new_msg_values.get('content_subtype') == 'html' else new_msg_values.get('body_text', '')
988         
989         # remove message writer
990         if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0:
991             user_to_notify_ids.remove(new_msg_values.get('user_id'))
992         
993         # get user_ids directly asked
994         user_to_push_from_parse_ids = self.message_parse_users(cr, uid, body, context=context)
995         
996         # try to find an email_to
997         email_to = ''
998         for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context):
999             if not user.notification_email_pref == 'all' and \
1000                 not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
1001                 continue
1002             if not user.user_email:
1003                 continue
1004             email_to = '%s, %s' % (email_to, user.user_email)
1005             email_to = email_to.lstrip(', ')
1006         
1007         # did not find any email address: not necessary to create an email
1008         if not email_to:
1009             return
1010         
1011         # try to find an email_from
1012         current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
1013         email_from = new_msg_values.get('email_from')
1014         if not email_from:
1015             email_from = current_user.user_email
1016         
1017         # get email content, create it (with mail_message.create)
1018         email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
1019         email_id = message_obj.create(cr, uid, email_values, context=context)
1020         return email_id
1021     
1022     def message_create_notify_get_email_dict(self, cr, uid, new_msg_values, email_from, email_to, context=None):
1023         values = dict(new_msg_values)
1024         
1025         body_html = new_msg_values.get('body_html', '')
1026         if body_html:
1027             body_html += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1028         body_text = new_msg_values.get('body_text', '')
1029         if body_text:
1030             body_text += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1031         values.update({
1032             'type': 'email',
1033             'state': 'outgoing',
1034             'email_from': email_from,
1035             'email_to': email_to,
1036             'subject': 'New message',
1037             'content_subtype': new_msg_values.get('content_subtype', 'plain'),
1038             'body_html': body_html,
1039             'body_text': body_text,
1040             'auto_delete': True,
1041             'res_model': '',
1042             'res_id': False,
1043         })
1044         return values
1045
1046     def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
1047         notif_obj = self.pool.get('mail.notification')
1048         msg_obj = self.pool.get('mail.message')
1049         if remove_childs:
1050             notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
1051         else:
1052             notif_msg_ids = msg_ids
1053         to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
1054         return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
1055
1056     #------------------------------------------------------
1057     # Thread_state
1058     #------------------------------------------------------
1059
1060     def message_create_set_unread(self, cr, uid, ids, context=None):
1061         """ When creating a new message, set as unread if uid is not the
1062             object responsible. """
1063         for obj in self.browse(cr, uid, ids, context=context):
1064             if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
1065                 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1066
1067     def message_check_and_set_unread(self, cr, uid, ids, context=None):
1068         """ Set unread if uid is the object responsible or if the object has
1069             no responsible. """
1070         for obj in self.browse(cr, uid, ids, context=context):
1071             if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
1072                 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1073
1074     def message_mark_as_unread(self, cr, uid, ids, context=None):
1075         """ Set as unread. """
1076         return self.write(cr, uid, ids, {'message_state': False}, context=context)
1077
1078     def message_check_and_set_read(self, cr, uid, ids, context=None):
1079         """ Set read if uid is the object responsible. """
1080         for obj in self.browse(cr, uid, ids, context=context):
1081             if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
1082                 self.message_mark_as_read(cr, uid, [obj.id], context=context)
1083     
1084     def message_mark_as_read(self, cr, uid, ids, context=None):
1085         """ Set as read. """
1086         return self.write(cr, uid, ids, {'message_state': True}, context=context)
1087
1088
1089 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: