1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2009-today OpenERP SA (<http://www.openerp.com>)
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
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
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/>
20 ##############################################################################
24 from email.utils import parsedate
26 from mail_message import decode, to_email
27 from operator import itemgetter
28 from osv import osv, fields
32 from tools.translate import _
35 _logger = logging.getLogger(__name__)
37 class mail_thread(osv.Model):
38 '''Mixin model, meant to be inherited by any model that needs to
39 act as a discussion topic on which messages can be attached.
40 Public methods are prefixed with ``message_`` in order to avoid
41 name collisions with methods of the models that will inherit
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.
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
63 _description = 'Email Thread'
65 def _get_message_ids(self, cr, uid, ids, name, args, context=None):
68 message_ids = self.message_search(cr, uid, [id], context=context)
69 subscriber_ids = self.message_get_subscribers(cr, uid, [id], context=context)
71 'message_ids': message_ids,
72 'message_summary': "<span><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (len(message_ids), len(subscriber_ids)),
76 def _search_message_ids(self, cr, uid, obj, name, args, context=None):
77 msg_obj = self.pool.get('mail.message')
78 msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
79 return [('id', 'in', msg_ids)]
82 'message_ids': fields.function(_get_message_ids, method=True,
83 fnct_search=_search_message_ids,
84 type='one2many', obj='mail.message', _fields_id = 'res_id',
85 string='Temp messages', multi="_get_message_ids",
86 help="Functional field holding messages related to the current document."),
87 'message_state': fields.boolean('Read',
88 help="When checked, new messages require your attention."),
89 'message_summary': fields.function(_get_message_ids, method=True,
90 type='text', string='Summary', multi="_get_message_ids",
91 help="Holds the Chatter summary (number of messages, ...). "\
92 "This summary is directly in html format in order to "\
93 "be inserted in kanban views."),
97 'message_state': True,
100 #------------------------------------------------------
101 # Automatic subscription when creating/reading
102 #------------------------------------------------------
104 def create(self, cr, uid, vals, context=None):
105 """Automatically subscribe the creator """
106 thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
108 self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
111 def write(self, cr, uid, ids, vals, context=None):
112 """Automatically subscribe the writer"""
113 if isinstance(ids, (int, long)):
115 write_res = super(mail_thread, self).write(cr, uid, ids, vals, context=context);
117 self.message_subscribe(cr, uid, ids, [uid], context=context)
120 def unlink(self, cr, uid, ids, context=None):
121 """Override unlink, to automatically delete
124 that are linked with res_model and res_id, not through
125 a foreign key with a 'cascade' ondelete attribute.
126 Notifications will be deleted with messages
128 subscr_obj = self.pool.get('mail.subscription')
129 msg_obj = self.pool.get('mail.message')
130 # delete subscriptions
131 subscr_to_del_ids = subscr_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
132 subscr_obj.unlink(cr, uid, subscr_to_del_ids, context=context)
133 # delete messages and notifications
134 msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
135 msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
137 return super(mail_thread, self).unlink(cr, uid, ids, context=context)
139 #------------------------------------------------------
140 # mail.message wrappers and tools
141 #------------------------------------------------------
143 def message_create(self, cr, uid, thread_id, vals, context=None):
144 """ OpenChatter: wrapper of mail.message create method
145 - creates the mail.message
146 - automatically subscribe the message writer
147 - push the message to subscribed users
152 message_obj = self.pool.get('mail.message')
153 notification_obj = self.pool.get('mail.notification')
154 body = vals.get('body_html', '') if vals.get('content_subtype') == 'html' else vals.get('body_text', '')
156 # automatically subscribe the writer of the message
158 self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
161 msg_id = message_obj.create(cr, uid, vals, context=context)
163 # Set as unread if writer is not the document responsible
164 self.message_create_set_unread(cr, uid, [thread_id], context=context)
166 # special: if install mode, do not push demo data
167 if context.get('install_mode', False):
170 # get users that will get a notification pushed
171 user_to_push_ids = self.message_get_user_ids_to_notify(cr, uid, [thread_id], vals, context=context)
172 for id in user_to_push_ids:
173 notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
175 # create the email to send
176 email_id = self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
180 def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
181 subscription_obj = self.pool.get('mail.subscription')
182 hide_obj = self.pool.get('mail.subscription.hide')
184 body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
187 user_sub_ids = self.message_get_subscribers(cr, uid, thread_ids, context=context)
189 # get hiden subscriptions
190 subscription_ids = subscription_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', thread_ids), ('user_id', 'in', user_sub_ids)], context=context)
191 hide_ids = hide_obj.search(cr, uid, [('subscription_id', 'in', subscription_ids), ('subtype', '=', new_msg_vals.get('subtype'))], context=context)
193 hiden_subscriptions = hide_obj.browse(cr, uid, hide_ids, context=context)
194 hiden_user_ids = [hiden_subscription.subscription_id.user_id.id for hiden_subscription in hiden_subscriptions]
195 notif_user_ids = [user_id for user_id in user_sub_ids if not user_id in hiden_user_ids]
197 notif_user_ids = user_sub_ids
199 # add users requested via parsing message (@login)
200 notif_user_ids += self.message_parse_users(cr, uid, body, context=context)
202 # add users requested to perform an action (need_action mechanism)
203 if hasattr(self, 'get_needaction_user_ids'):
204 user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context)
205 for id, user_ids in user_ids_dict.iteritems():
206 notif_user_ids += user_ids
208 # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
209 if new_msg_vals.get('parent_id'):
210 notif_obj = self.pool.get('mail.notification')
211 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
212 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
213 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
215 # remove duplicate entries
216 notif_user_ids = list(set(notif_user_ids))
217 return notif_user_ids
219 def message_parse_users(self, cr, uid, string, context=None):
220 """Parse message content
221 - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
222 this supports login that are emails (such as @raoul@grobedon.net)
224 regex = re.compile('(^|\s)@((\w|@|\.)*)')
225 login_lst = [item[1] for item in regex.findall(string)]
226 if not login_lst: return []
227 user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
230 #------------------------------------------------------
231 # Generic message api
232 #------------------------------------------------------
234 def message_capable_models(self, cr, uid, context=None):
236 for model_name in self.pool.obj_list():
237 model = self.pool.get(model_name)
238 if 'mail.thread' in getattr(model, '_inherit', []):
239 ret_dict[model_name] = model._description
242 def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
243 type='email', subtype=None, email_date=None, parent_id=False,
244 content_subtype='plain', state=None,
245 partner_ids=None, email_from=False, email_to=False,
246 email_cc=None, email_bcc=None, reply_to=None,
247 headers=None, message_id=False, references=None,
248 attachments=None, original=None, context=None):
249 """ Creates a new mail.message through message_create. The new message
250 is attached to the current mail.thread, containing all the details
251 passed as parameters. All attachments will be attached to the
252 thread record as well as to the actual message.
254 This method calls message_create that will handle management of
255 subscription and notifications, and effectively create the message.
257 If ``email_from`` is not set or ``type`` not set as 'email',
258 a note message is created (comment or system notification),
259 without the usual envelope attributes (sender, recipients, etc.).
261 :param threads: list of thread ids, or list of browse_records
262 representing threads to which a new message should be attached
263 :param subject: subject of the message, or description of the event;
264 this is totally optional as subjects are not important except
265 for specific messages (blog post, job offers) or for emails
266 :param body_text: plaintext contents of the mail or log message
267 :param body_html: html contents of the mail or log message
268 :param type: type of message: 'email', 'comment', 'notification';
270 :param subtype: subtype of message, such as 'create' or 'cancel'
271 :param email_date: email date string if different from now, in
273 :param parent_id: id of the parent message (threaded messaging model)
274 :param content_subtype: optional content_subtype of message: 'plain'
275 or 'html', corresponding to the main body contents (body_text or
277 :param state: state of message
278 :param partner_ids: destination partners of the message, in addition
279 to the now fully optional email_to; this method is supposed to
280 received a list of ids is not None. The specific many2many
281 instruction will be generated by this method.
282 :param email_from: Email From / Sender address if any
283 :param email_to: Email-To / Recipient address
284 :param email_cc: Comma-Separated list of Carbon Copy Emails To
286 :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To
288 :param reply_to: reply_to header
289 :param headers: mail headers to store
290 :param message_id: optional email identifier
291 :param references: optional email references
292 :param dict attachments: map of attachment filenames to binary
294 :param str original: optional full source of the RFC2822 email, for
296 :param dict context: if a ``thread_model`` value is present in the
297 context, its value will be used to determine the model of the
298 thread to update (instead of the current model).
302 if attachments is None:
306 edate = parsedate(email_date)
307 if edate is not None:
308 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
310 if all(isinstance(thread_id, (int, long)) for thread_id in threads):
311 model = context.get('thread_model') or self._name
312 model_pool = self.pool.get(model)
313 threads = model_pool.browse(cr, uid, threads, context=context)
315 ir_attachment = self.pool.get('ir.attachment')
316 mail_message = self.pool.get('mail.message')
319 for thread in threads:
321 for attachment in attachments:
322 fname, fcontent = attachment
323 if isinstance(fcontent, unicode):
324 fcontent = fcontent.encode('utf-8')
327 'datas': base64.b64encode(str(fcontent)),
328 'datas_fname': fname,
329 'description': _('Mail attachment'),
330 'res_model': thread._name,
333 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
334 # find related partner: partner_id column in thread object, or self is res.partner model
335 partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
336 if not partner_id and thread._name == 'res.partner':
337 partner_id = thread.id
338 # destination partners
339 if partner_ids is None:
341 mail_partner_ids = [6, 0, partner_ids]
345 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
346 'body_html': body_html or '',
347 'parent_id': parent_id,
348 'date': email_date or fields.datetime.now(),
351 'content_subtype': content_subtype,
353 'message_id': message_id,
354 'partner_ids': mail_partner_ids,
355 'attachment_ids': [(6, 0, to_attach)],
357 'model' : thread._name,
359 'partner_id': partner_id,
362 if email_from or type == 'email':
363 for param in (email_to, email_cc, email_bcc):
364 if isinstance(param, list):
365 param = ", ".join(param)
367 'email_to': email_to,
368 'email_from': email_from or \
369 (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
370 'email_cc': email_cc,
371 'email_bcc': email_bcc,
372 'references': references,
374 'reply_to': reply_to,
375 'original': original, })
377 new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
380 def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
381 """Creates a new mail.message attached to the given threads (``ids``),
382 with the contents of ``msg_dict``, by calling ``message_append``
383 with the mail details. All attachments in msg_dict will be
384 attached to the object record as well as to the actual
387 :param dict msg_dict: a map containing the email details and
388 attachments. See ``message_process()`` and
389 ``mail.message.parse()`` for details on
391 :param dict context: if a ``thread_model`` value is present
392 in the context, its value will be used
393 to determine the model of the thread to
394 update (instead of the current model).
396 return self.message_append(cr, uid, ids,
397 subject = msg_dict.get('subject'),
398 body_text = msg_dict.get('body_text'),
399 body_html= msg_dict.get('body_html'),
400 parent_id = msg_dict.get('parent_id', False),
401 type = msg_dict.get('type', 'email'),
402 subtype = msg_dict.get('subtype'),
403 content_subtype = msg_dict.get('content_subtype'),
404 state = msg_dict.get('state'),
405 partner_ids = msg_dict.get('partner_ids'),
406 email_from = msg_dict.get('from', msg_dict.get('email_from')),
407 email_to = msg_dict.get('to', msg_dict.get('email_to')),
408 email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
409 email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
410 reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
411 email_date = msg_dict.get('date'),
412 message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
413 references = msg_dict.get('references')\
414 or msg_dict.get('in-reply-to'),
415 attachments = msg_dict.get('attachments'),
416 headers = msg_dict.get('headers'),
417 original = msg_dict.get('original'),
420 #------------------------------------------------------
422 #------------------------------------------------------
424 def _message_search_ancestor_ids(self, cr, uid, ids, child_ids, ancestor_ids, context=None):
425 """ Given message child_ids ids, find their ancestors until ancestor_ids
426 using their parent_id relationship.
428 :param child_ids: the first nodes of the search
429 :param ancestor_ids: list of ancestors. When the search reach an
432 def _get_parent_ids(message_list, ancestor_ids, child_ids):
433 """ Tool function: return the list of parent_ids of messages
434 contained in message_list. Parents that are in ancestor_ids
435 or in child_ids are not returned. """
436 return [message['parent_id'][0] for message in message_list
437 if message['parent_id']
438 and message['parent_id'][0] not in ancestor_ids
439 and message['parent_id'][0] not in child_ids
442 message_obj = self.pool.get('mail.message')
443 messages_temp = message_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
444 parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
445 child_ids += parent_ids
446 cur_iter = 0; max_iter = 100; # avoid infinite loop
447 while (parent_ids and (cur_iter < max_iter)):
449 messages_temp = message_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
450 parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
451 child_ids += parent_ids
452 if (cur_iter > max_iter):
453 _logger.warning("Possible infinite loop in _message_search_ancestor_ids. "\
454 "Note that this algorithm is intended to check for cycle in "\
455 "message graph, leading to a curious error. Have fun.")
458 def message_search_get_domain(self, cr, uid, ids, context=None):
459 """ OpenChatter feature: get the domain to search the messages related
460 to a document. mail.thread defines the default behavior as
461 being messages with model = self._name, id in ids.
462 This method should be overridden if a model has to implement a
465 return ['&', ('res_id', 'in', ids), ('model', '=', self._name)]
467 def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
468 limit=100, offset=0, domain=None, count=False, context=None):
469 """ OpenChatter feature: return thread messages ids according to the
470 search domain given by ``message_search_get_domain``.
472 It is possible to add in the search the parent of messages by
473 setting the fetch_ancestors flag to True. In that case, using
474 the parent_id relationship, the method returns the id list according
475 to the search domain, but then calls ``_message_search_ancestor_ids``
476 that will add to the list the ancestors ids. The search is limited
477 to parent messages having an id in ancestor_ids or having
478 parent_id set to False.
480 If ``count==True``, the number of ids is returned instead of the
481 id list. The count is done by hand instead of passing it as an
482 argument to the search call because we might want to perform
483 a research including parent messages until some ancestor_ids.
485 :param fetch_ancestors: performs an ascended search; will add
486 to fetched msgs all their parents until
488 :param ancestor_ids: used when fetching ancestors
489 :param domain: domain to add to the search; especially child_of
490 is interesting when dealing with threaded display.
491 Note that the added domain is anded with the
493 :param limit, offset, count, context: as usual
495 search_domain = self.message_search_get_domain(cr, uid, ids, context=context)
497 search_domain += domain
498 message_obj = self.pool.get('mail.message')
499 message_res = message_obj.search(cr, uid, search_domain, limit=limit, offset=offset, count=count, context=context)
500 if not count and fetch_ancestors:
501 message_res += self._message_search_ancestor_ids(cr, uid, ids, message_res, ancestor_ids, context=context)
504 def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
505 limit=100, offset=0, domain=None, context=None):
506 """ OpenChatter feature: read the messages related to some threads.
507 This method is used mainly the Chatter widget, to directly have
508 read result instead of searching then reading.
510 Please see message_search for more information about the parameters.
512 message_ids = self.message_search(cr, uid, ids, fetch_ancestors, ancestor_ids,
513 limit, offset, domain, context=context)
514 messages = self.pool.get('mail.message').read(cr, uid, message_ids, context=context)
516 """ Retrieve all attachments names """
517 map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
521 for attach_id in msg["attachment_ids"]:
522 map_id_to_name[attach_id] = '' # use empty string as a placeholder
525 ids = map_id_to_name.keys()
526 names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
528 # convert the list of tuples into a dictionnary
530 map_id_to_name[name[0]] = name[1]
532 # give corresponding ids and names to each message
534 msg["attachments"] = []
536 for attach_id in msg["attachment_ids"]:
537 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
539 # Set the threads as read
540 self.message_check_and_set_read(cr, uid, ids, context=context)
541 # Sort and return the messages
542 messages = sorted(messages, key=lambda d: (-d['id']))
545 def message_get_pushed_messages(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
546 limit=100, offset=0, msg_search_domain=[], context=None):
547 """ OpenChatter: wall: get the pushed notifications and used them
548 to fetch messages to display on the wall.
550 :param fetch_ancestors: performs an ascended search; will add
551 to fetched msgs all their parents until
553 :param ancestor_ids: used when fetching ancestors
554 :param domain: domain to add to the search; especially child_of
555 is interesting when dealing with threaded display
556 :param ascent: performs an ascended search; will add to fetched msgs
557 all their parents until root_ids
558 :param root_ids: for ascent search
559 :return: list of mail.messages sorted by date
561 notification_obj = self.pool.get('mail.notification')
562 msg_obj = self.pool.get('mail.message')
563 # update message search
564 for arg in msg_search_domain:
565 if isinstance(arg, (tuple, list)):
566 arg[0] = 'message_id.' + arg[0]
567 # compose final domain
568 domain = [('user_id', '=', uid)] + msg_search_domain
570 notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
571 notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
572 msg_ids = [notification.message_id.id for notification in notifications]
574 msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
575 if (fetch_ancestors): msg_ids = self._message_search_ancestor_ids(cr, uid, ids, msg_ids, ancestor_ids, context=context)
576 msgs = msg_obj.read(cr, uid, msg_ids, context=context)
579 #------------------------------------------------------
581 #------------------------------------------------------
582 # message_process will call either message_new or message_update.
584 def message_process(self, cr, uid, model, message, custom_values=None,
585 save_original=False, strip_attachments=False,
587 """Process an incoming RFC2822 email message related to the
588 given thread model, relying on ``mail.message.parse()``
589 for the parsing operation, and then calling ``message_new``
590 (if the thread record did not exist) or ``message_update``
591 (if it did), then calling ``message_forward`` to automatically
592 notify other people that should receive this message.
594 :param string model: the thread model for which a new message
596 :param message: source of the RFC2822 mail
597 :type message: string or xmlrpclib.Binary
598 :type dict custom_values: optional dictionary of field values
599 to pass to ``message_new`` if a new
600 record needs to be created. Ignored
601 if the thread record already exists.
602 :param bool save_original: whether to keep a copy of the original
603 email source attached to the message after it is imported.
604 :param bool strip_attachments: whether to strip all attachments
605 before processing the message, in order to save some space.
607 # extract message bytes - we are forced to pass the message as binary because
608 # we don't know its encoding until we parse its headers and hence can't
609 # convert it to utf-8 for transport between the mailgate script and here.
610 if isinstance(message, xmlrpclib.Binary):
611 message = str(message.data)
613 model_pool = self.pool.get(model)
614 if self._name != model:
615 if context is None: context = {}
616 context.update({'thread_model': model})
618 mail_message = self.pool.get('mail.message')
621 # Warning: message_from_string doesn't always work correctly on unicode,
622 # we must use utf-8 strings here :-(
623 if isinstance(message, unicode):
624 message = message.encode('utf-8')
625 msg_txt = email.message_from_string(message)
626 msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context)
629 msg['state'] = 'received'
631 if strip_attachments and 'attachments' in msg:
632 del msg['attachments']
634 # Create New Record into particular model
635 def create_record(msg):
636 if hasattr(model_pool, 'message_new'):
637 return model_pool.message_new(cr, uid, msg,
641 if msg.get('references') or msg.get('in-reply-to'):
642 references = msg.get('references') or msg.get('in-reply-to')
643 if '\r\n' in references:
644 references = references.split('\r\n')
646 references = references.split(' ')
647 for ref in references:
649 res_id = tools.reference_re.search(ref)
651 res_id = res_id.group(1)
653 res_id = tools.res_re.search(msg['subject'])
655 res_id = res_id.group(1)
658 if model_pool.exists(cr, uid, res_id):
659 if hasattr(model_pool, 'message_update'):
660 model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
662 # referenced thread was not found, we'll have to create a new one
665 res_id = create_record(msg)
666 # To forward the email to other followers
667 self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
669 model_pool.message_mark_as_unread(cr, uid, [res_id], context=context)
672 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
673 """Called by ``message_process`` when a new message is received
674 for a given thread model, if the message did not belong to
676 The default behavior is to create a new record of the corresponding
677 model (based on some very basic info extracted from the message),
678 then attach the message to the newly created record
679 (by calling ``message_append_dict``).
680 Additional behavior may be implemented by overriding this method.
682 :param dict msg_dict: a map containing the email details and
683 attachments. See ``message_process`` and
684 ``mail.message.parse`` for details.
685 :param dict custom_values: optional dictionary of additional
686 field values to pass to create()
687 when creating the new thread record.
688 Be careful, these values may override
689 any other values coming from the message.
690 :param dict context: if a ``thread_model`` value is present
691 in the context, its value will be used
692 to determine the model of the record
693 to create (instead of the current model).
695 :return: the id of the newly created thread object
699 model = context.get('thread_model') or self._name
700 model_pool = self.pool.get(model)
701 fields = model_pool.fields_get(cr, uid, context=context)
702 data = model_pool.default_get(cr, uid, fields, context=context)
703 if 'name' in fields and not data.get('name'):
704 data['name'] = msg_dict.get('from', '')
705 if custom_values and isinstance(custom_values, dict):
706 data.update(custom_values)
707 res_id = model_pool.create(cr, uid, data, context=context)
708 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
711 def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
712 """ Called by ``message_process`` when a new message is received
713 for an existing thread. The default behavior is to create a
714 new mail.message in the given thread (by calling
715 ``message_append_dict``)
716 Additional behavior may be implemented by overriding this
719 :param dict msg_dict: a map containing the email details and
720 attachments. See ``message_process`` and
721 ``mail.message.parse()`` for details.
722 :param dict vals: a dict containing values to update records
723 given their ids; if the dict is None or is
724 void, no write operation is performed.
725 :param dict context: if a ``thread_model`` value is present
726 in the context, its value will be used
727 to determine the model of the thread to
728 update (instead of the current model).
731 self.write(cr, uid, ids, update_vals, context=context)
732 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
734 def message_thread_followers(self, cr, uid, ids, context=None):
735 """ Returns a list of email addresses of the people following
736 this thread, including the sender of each mail, and the
737 people who were in CC of the messages, if any.
740 if isinstance(ids, (str, int, long)):
742 for thread in self.browse(cr, uid, ids, context=context):
744 for message in thread.message_ids:
745 l.add((message.user_id and message.user_id.user_email) or '')
746 l.add(message.email_from or '')
747 l.add(message.email_cc or '')
748 res[thread.id] = filter(None, l)
751 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
752 """Sends an email to all people following the given threads.
753 The emails are forwarded immediately, not queued for sending,
756 :param str model: thread model
757 :param list thread_ids: ids of the thread records
758 :param msg: email.message.Message object to forward
759 :param email_error: optional email address to notify in case
760 of any delivery error during the forward.
763 model_pool = self.pool.get(model)
764 smtp_server_obj = self.pool.get('ir.mail_server')
765 mail_message = self.pool.get('mail.message')
766 for res in model_pool.browse(cr, uid, thread_ids, context=context):
767 if hasattr(model_pool, 'message_thread_followers'):
768 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
770 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
771 message_followers_emails = to_email(','.join(filter(None, followers)))
772 message_recipients = to_email(','.join(filter(None,
773 [decode(msg['from']),
775 decode(msg['cc'])])))
776 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
778 # TODO: we need an interface for this for all types of objects, not just leads
779 if hasattr(res, 'section_id'):
781 msg['reply-to'] = res.section_id.reply_to
783 smtp_from, = to_email(msg['from'])
784 msg['from'] = smtp_from
785 msg['to'] = ", ".join(forward_to)
786 msg['message-id'] = tools.generate_tracking_message_id(res.id)
787 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
788 subj = msg['subject']
789 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
790 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
791 msg['to'] = email_error
792 smtp_server_obj.send_email(cr, uid, msg)
795 def message_partner_by_email(self, cr, uid, email, context=None):
796 """Attempts to return the id of a partner address matching
797 the given ``email``, and the corresponding partner id.
798 Can be used by classes using the ``mail.thread`` mixin
799 to lookup the partner and use it in their implementation
800 of ``message_new`` to link the new record with a
801 corresponding partner.
802 The keys used in the returned dict are meant to map
803 to usual names for relationships towards a partner
804 and one of its addresses.
806 :param email: email address for which a partner
807 should be searched for.
809 :return: a map of the following form::
811 { 'partner_address_id': id or False,
812 'partner_id': pid or False }
814 partner_pool = self.pool.get('res.partner')
815 res = {'partner_id': False}
817 email = to_email(email)[0]
818 contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
820 contact = partner_pool.browse(cr, uid, contact_ids[0])
821 res['partner_id'] = contact.id
824 # for backwards-compatibility with old scripts
825 process_email = message_process
827 #------------------------------------------------------
829 #------------------------------------------------------
831 def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', content_subtype='html', context=None):
834 notification_obj = self.pool.get('mail.notification')
836 msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, content_subtype=content_subtype, context=context)
837 # escape if in install mode or note writing was not successfull
838 if 'install_mode' in context:
840 if not isinstance(msg_ids, (list)):
842 # get already existing notigications
843 notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
844 already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
845 # get base.group_user group
846 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
847 group_id = res and res[1] or False
848 if not group_id: return True
849 group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
850 for user in group.users:
851 if user.id in already_pushed_user_ids: continue
852 for msg_id in msg_ids:
853 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
856 def log(self, cr, uid, id, message, secondary=False, context=None):
857 _logger.warning("log() is deprecated. As this module inherit from \
858 mail.thread, the message will be managed by this \
859 module instead of by the res.log mechanism. Please \
860 use the mail.thread OpenChatter API instead of the \
861 now deprecated res.log.")
862 self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
864 def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False,
865 type='notification', content_subtype='html', subtype=None, context=None):
866 if type in ['notification', 'comment']:
868 if content_subtype == 'html':
874 return self.message_append(cr, uid, ids, subject, body_html, body_text,
875 type, subtype, parent_id=parent_id,
876 content_subtype=content_subtype, context=context)
878 #------------------------------------------------------
879 # Subscription mechanism
880 #------------------------------------------------------
882 def message_get_subscribers(self, cr, uid, ids, context=None):
883 """ Returns the current document followers. Basically this method
884 checks in mail.subscription for entries with matching res_model,
886 This method can be overriden to add implicit subscribers, such
887 as project managers, by adding their user_id to the list of
888 ids returned by this method.
890 subscr_obj = self.pool.get('mail.subscription')
891 subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
892 return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
894 def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
895 """ Returns the current document followers as a read result. Used
896 mainly for Chatter having only one method to call to have
899 user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
900 return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
902 def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
903 """ Check if uid or user_id (if set) is a subscriber to the current
906 :param user_id: if set, check is done on user_id; if not set
909 sub_user_id = uid if user_id is None else user_id
910 if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
914 def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
915 """ Subscribe the user (or user_ids) to the current document.
917 :param user_ids: a list of user_ids; if not set, subscribe
920 subscription_obj = self.pool.get('mail.subscription')
921 to_subscribe_uids = [uid] if user_ids is None else user_ids
924 already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
925 for user_id in to_subscribe_uids:
926 if user_id in already_subscribed_user_ids: continue
927 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
930 def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
931 """ Unsubscribe the user (or user_ids) from the current document.
933 :param user_ids: a list of user_ids; if not set, subscribe
936 # Trying to unsubscribe somebody not in subscribers: returns False
937 # if special management is needed; allows to know that an automatically
938 # subscribed user tries to unsubscribe and allows to warn him
939 to_unsubscribe_uids = [uid] if user_ids is None else user_ids
940 subscription_obj = self.pool.get('mail.subscription')
941 to_delete_sub_ids = subscription_obj.search(cr, uid,
942 ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
943 if not to_delete_sub_ids:
945 return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
947 def message_subscription_hide(self, cr, uid, ids, subtype=None, context=None):
948 """Hide notifications, allowing to tune the messages displayed on
950 :param subtype: a mail.message subtype. If None, it is means the
951 user wants to hide all notifications coming from
954 subscription_obj = self.pool.get('mail.subscription')
955 subscription_hide_obj = self.pool.get('mail.subscription.hide')
956 # find the related subscriptions
957 subscription_ids = subscription_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', '=', uid)], context=context)
959 for subscription_id in subscription_ids:
960 subscription_hide_obj.create(cr, uid, {'subscription_id': subscription_id, 'subtype': subtype}, context=context)
963 #------------------------------------------------------
965 #------------------------------------------------------
967 def message_create_notify_by_email(self, cr, uid, new_msg_values, user_to_notify_ids, context=None):
968 """ When creating a new message and pushing notifications, emails
969 must be send if users have chosen to receive notifications
970 by email via the notification_email_pref field.
972 ``notification_email_pref`` can have 3 values :
973 - all: receive all notification by email (for example for shared
975 - to_me: messages send directly to me (@login, messages on res.users)
976 - never: never receive notifications
977 Note that an user should never receive notifications for messages
980 :param new_msg_values: dictionary of message values, those that
981 are given to the create method
982 :param user_to_notify_ids: list of user_ids, user that will
983 receive a notification on their Wall
985 message_obj = self.pool.get('mail.message')
986 res_users_obj = self.pool.get('res.users')
987 body = new_msg_values.get('body_html', '') if new_msg_values.get('content_subtype') == 'html' else new_msg_values.get('body_text', '')
989 # remove message writer
990 if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0:
991 user_to_notify_ids.remove(new_msg_values.get('user_id'))
993 # get user_ids directly asked
994 user_to_push_from_parse_ids = self.message_parse_users(cr, uid, body, context=context)
996 # try to find an email_to
998 for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context):
999 if not user.notification_email_pref == 'all' and \
1000 not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
1002 if not user.user_email:
1004 email_to = '%s, %s' % (email_to, user.user_email)
1005 email_to = email_to.lstrip(', ')
1007 # did not find any email address: not necessary to create an email
1011 # try to find an email_from
1012 current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
1013 email_from = new_msg_values.get('email_from')
1015 email_from = current_user.user_email
1017 # get email content, create it (with mail_message.create)
1018 email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
1019 email_id = message_obj.create(cr, uid, email_values, context=context)
1022 def message_create_notify_get_email_dict(self, cr, uid, new_msg_values, email_from, email_to, context=None):
1023 values = dict(new_msg_values)
1025 body_html = new_msg_values.get('body_html', '')
1027 body_html += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1028 body_text = new_msg_values.get('body_text', '')
1030 body_text += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1033 'state': 'outgoing',
1034 'email_from': email_from,
1035 'email_to': email_to,
1036 'subject': 'New message',
1037 'content_subtype': new_msg_values.get('content_subtype', 'plain'),
1038 'body_html': body_html,
1039 'body_text': body_text,
1040 'auto_delete': True,
1046 def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
1047 notif_obj = self.pool.get('mail.notification')
1048 msg_obj = self.pool.get('mail.message')
1050 notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
1052 notif_msg_ids = msg_ids
1053 to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
1054 return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
1056 #------------------------------------------------------
1058 #------------------------------------------------------
1060 def message_create_set_unread(self, cr, uid, ids, context=None):
1061 """ When creating a new message, set as unread if uid is not the
1062 object responsible. """
1063 for obj in self.browse(cr, uid, ids, context=context):
1064 if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
1065 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1067 def message_check_and_set_unread(self, cr, uid, ids, context=None):
1068 """ Set unread if uid is the object responsible or if the object has
1070 for obj in self.browse(cr, uid, ids, context=context):
1071 if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
1072 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1074 def message_mark_as_unread(self, cr, uid, ids, context=None):
1075 """ Set as unread. """
1076 return self.write(cr, uid, ids, {'message_state': False}, context=context)
1078 def message_check_and_set_read(self, cr, uid, ids, context=None):
1079 """ Set read if uid is the object responsible. """
1080 for obj in self.browse(cr, uid, ids, context=context):
1081 if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
1082 self.message_mark_as_read(cr, uid, [obj.id], context=context)
1084 def message_mark_as_read(self, cr, uid, ids, context=None):
1085 """ Set as read. """
1086 return self.write(cr, uid, ids, {'message_state': True}, context=context)
1089 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: