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