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