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')
183 body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
186 notif_user_ids = self.message_get_subscribers(cr, uid, thread_ids, context=context)
188 # add users requested via parsing message (@login)
189 notif_user_ids += self.message_parse_users(cr, uid, body, context=context)
191 # add users requested to perform an action (need_action mechanism)
192 if hasattr(self, 'get_needaction_user_ids'):
193 user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context)
194 for id, user_ids in user_ids_dict.iteritems():
195 notif_user_ids += user_ids
197 # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
198 if new_msg_vals.get('parent_id'):
199 notif_obj = self.pool.get('mail.notification')
200 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
201 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
202 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
204 # remove duplicate entries
205 notif_user_ids = list(set(notif_user_ids))
206 return notif_user_ids
208 def message_parse_users(self, cr, uid, string, context=None):
209 """Parse message content
210 - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
211 this supports login that are emails (such as @raoul@grobedon.net)
213 regex = re.compile('(^|\s)@((\w|@|\.)*)')
214 login_lst = [item[1] for item in regex.findall(string)]
215 if not login_lst: return []
216 user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
219 #------------------------------------------------------
220 # Generic message api
221 #------------------------------------------------------
223 def message_capable_models(self, cr, uid, context=None):
225 for model_name in self.pool.obj_list():
226 model = self.pool.get(model_name)
227 if 'mail.thread' in getattr(model, '_inherit', []):
228 ret_dict[model_name] = model._description
231 def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
232 type='email', email_date=None, parent_id=False,
233 content_subtype='plain', state=None,
234 partner_ids=None, email_from=False, email_to=False,
235 email_cc=None, email_bcc=None, reply_to=None,
236 headers=None, message_id=False, references=None,
237 attachments=None, original=None, context=None):
238 """ Creates a new mail.message through message_create. The new message
239 is attached to the current mail.thread, containing all the details
240 passed as parameters. All attachments will be attached to the
241 thread record as well as to the actual message.
243 This method calls message_create that will handle management of
244 subscription and notifications, and effectively create the message.
246 If ``email_from`` is not set or ``type`` not set as 'email',
247 a note message is created (comment or system notification),
248 without the usual envelope attributes (sender, recipients, etc.).
250 :param threads: list of thread ids, or list of browse_records
251 representing threads to which a new message should be attached
252 :param subject: subject of the message, or description of the event;
253 this is totally optional as subjects are not important except
254 for specific messages (blog post, job offers) or for emails
255 :param body_text: plaintext contents of the mail or log message
256 :param body_html: html contents of the mail or log message
257 :param type: type of message: 'email', 'comment', 'notification';
259 :param email_date: email date string if different from now, in
261 :param parent_id: id of the parent message (threaded messaging model)
262 :param content_subtype: optional content_subtype of message: 'plain'
263 or 'html', corresponding to the main body contents (body_text or
265 :param state: state of message
266 :param partner_ids: destination partners of the message, in addition
267 to the now fully optional email_to; this method is supposed to
268 received a list of ids is not None. The specific many2many
269 instruction will be generated by this method.
270 :param email_from: Email From / Sender address if any
271 :param email_to: Email-To / Recipient address
272 :param email_cc: Comma-Separated list of Carbon Copy Emails To
274 :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To
276 :param reply_to: reply_to header
277 :param headers: mail headers to store
278 :param message_id: optional email identifier
279 :param references: optional email references
280 :param dict attachments: map of attachment filenames to binary
282 :param str original: optional full source of the RFC2822 email, for
284 :param dict context: if a ``thread_model`` value is present in the
285 context, its value will be used to determine the model of the
286 thread to update (instead of the current model).
290 if attachments is None:
294 edate = parsedate(email_date)
295 if edate is not None:
296 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
298 if all(isinstance(thread_id, (int, long)) for thread_id in threads):
299 model = context.get('thread_model') or self._name
300 model_pool = self.pool.get(model)
301 threads = model_pool.browse(cr, uid, threads, context=context)
303 ir_attachment = self.pool.get('ir.attachment')
304 mail_message = self.pool.get('mail.message')
307 for thread in threads:
309 for attachment in attachments:
310 fname, fcontent = attachment
311 if isinstance(fcontent, unicode):
312 fcontent = fcontent.encode('utf-8')
315 'datas': base64.b64encode(str(fcontent)),
316 'datas_fname': fname,
317 'description': _('Mail attachment'),
318 'res_model': thread._name,
321 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
322 # find related partner: partner_id column in thread object, or self is res.partner model
323 partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
324 if not partner_id and thread._name == 'res.partner':
325 partner_id = thread.id
326 # destination partners
327 if partner_ids is None:
329 mail_partner_ids = [(6, 0, partner_ids)]
333 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
334 'body_html': body_html or '',
335 'parent_id': parent_id,
336 'date': email_date or fields.datetime.now(),
338 'content_subtype': content_subtype,
340 'message_id': message_id,
341 'partner_ids': mail_partner_ids,
342 'attachment_ids': [(6, 0, to_attach)],
344 'model' : thread._name,
346 'partner_id': partner_id,
349 if email_from or type == 'email':
350 for param in (email_to, email_cc, email_bcc):
351 if isinstance(param, list):
352 param = ", ".join(param)
354 'email_to': email_to,
355 'email_from': email_from or \
356 (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
357 'email_cc': email_cc,
358 'email_bcc': email_bcc,
359 'references': references,
361 'reply_to': reply_to,
362 'original': original, })
364 new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
367 def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
368 """Creates a new mail.message attached to the given threads (``ids``),
369 with the contents of ``msg_dict``, by calling ``message_append``
370 with the mail details. All attachments in msg_dict will be
371 attached to the object record as well as to the actual
374 :param dict msg_dict: a map containing the email details and
375 attachments. See ``message_process()`` and
376 ``mail.message.parse()`` for details on
378 :param dict context: if a ``thread_model`` value is present
379 in the context, its value will be used
380 to determine the model of the thread to
381 update (instead of the current model).
383 return self.message_append(cr, uid, ids,
384 subject = msg_dict.get('subject'),
385 body_text = msg_dict.get('body_text'),
386 body_html= msg_dict.get('body_html'),
387 parent_id = msg_dict.get('parent_id', False),
388 type = msg_dict.get('type', 'email'),
389 content_subtype = msg_dict.get('content_subtype'),
390 state = msg_dict.get('state'),
391 partner_ids = msg_dict.get('partner_ids'),
392 email_from = msg_dict.get('from', msg_dict.get('email_from')),
393 email_to = msg_dict.get('to', msg_dict.get('email_to')),
394 email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
395 email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
396 reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
397 email_date = msg_dict.get('date'),
398 message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
399 references = msg_dict.get('references')\
400 or msg_dict.get('in-reply-to'),
401 attachments = msg_dict.get('attachments'),
402 headers = msg_dict.get('headers'),
403 original = msg_dict.get('original'),
406 #------------------------------------------------------
408 #------------------------------------------------------
410 def _message_search_ancestor_ids(self, cr, uid, ids, child_ids, ancestor_ids, context=None):
411 """ Given message child_ids ids, find their ancestors until ancestor_ids
412 using their parent_id relationship.
414 :param child_ids: the first nodes of the search
415 :param ancestor_ids: list of ancestors. When the search reach an
418 def _get_parent_ids(message_list, ancestor_ids, child_ids):
419 """ Tool function: return the list of parent_ids of messages
420 contained in message_list. Parents that are in ancestor_ids
421 or in child_ids are not returned. """
422 return [message['parent_id'][0] for message in message_list
423 if message['parent_id']
424 and message['parent_id'][0] not in ancestor_ids
425 and message['parent_id'][0] not in child_ids
428 message_obj = self.pool.get('mail.message')
429 messages_temp = message_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
430 parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
431 child_ids += parent_ids
432 cur_iter = 0; max_iter = 100; # avoid infinite loop
433 while (parent_ids and (cur_iter < max_iter)):
435 messages_temp = message_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
436 parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
437 child_ids += parent_ids
438 if (cur_iter > max_iter):
439 _logger.warning("Possible infinite loop in _message_search_ancestor_ids. "\
440 "Note that this algorithm is intended to check for cycle in "\
441 "message graph, leading to a curious error. Have fun.")
444 def message_search_get_domain(self, cr, uid, ids, context=None):
445 """ OpenChatter feature: get the domain to search the messages related
446 to a document. mail.thread defines the default behavior as
447 being messages with model = self._name, id in ids.
448 This method should be overridden if a model has to implement a
451 return ['&', ('res_id', 'in', ids), ('model', '=', self._name)]
453 def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
454 limit=100, offset=0, domain=None, count=False, context=None):
455 """ OpenChatter feature: return thread messages ids according to the
456 search domain given by ``message_search_get_domain``.
458 It is possible to add in the search the parent of messages by
459 setting the fetch_ancestors flag to True. In that case, using
460 the parent_id relationship, the method returns the id list according
461 to the search domain, but then calls ``_message_search_ancestor_ids``
462 that will add to the list the ancestors ids. The search is limited
463 to parent messages having an id in ancestor_ids or having
464 parent_id set to False.
466 If ``count==True``, the number of ids is returned instead of the
467 id list. The count is done by hand instead of passing it as an
468 argument to the search call because we might want to perform
469 a research including parent messages until some ancestor_ids.
471 :param fetch_ancestors: performs an ascended search; will add
472 to fetched msgs all their parents until
474 :param ancestor_ids: used when fetching ancestors
475 :param domain: domain to add to the search; especially child_of
476 is interesting when dealing with threaded display.
477 Note that the added domain is anded with the
479 :param limit, offset, count, context: as usual
481 search_domain = self.message_search_get_domain(cr, uid, ids, context=context)
483 search_domain += domain
484 message_obj = self.pool.get('mail.message')
485 message_res = message_obj.search(cr, uid, search_domain, limit=limit, offset=offset, count=count, context=context)
486 if not count and fetch_ancestors:
487 message_res += self._message_search_ancestor_ids(cr, uid, ids, message_res, ancestor_ids, context=context)
490 def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
491 limit=100, offset=0, domain=None, context=None):
492 """ OpenChatter feature: read the messages related to some threads.
493 This method is used mainly the Chatter widget, to directly have
494 read result instead of searching then reading.
496 Please see message_search for more information about the parameters.
498 message_ids = self.message_search(cr, uid, ids, fetch_ancestors, ancestor_ids,
499 limit, offset, domain, context=context)
500 messages = self.pool.get('mail.message').read(cr, uid, message_ids, context=context)
502 """ Retrieve all attachments names """
503 map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
506 for attach_id in msg["attachment_ids"]:
507 map_id_to_name[attach_id] = '' # use empty string as a placeholder
509 ids = map_id_to_name.keys()
510 names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
512 # convert the list of tuples into a dictionnary
514 map_id_to_name[name[0]] = name[1]
516 # give corresponding ids and names to each message
518 msg["attachments"] = []
520 for attach_id in msg["attachment_ids"]:
521 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
523 # Set the threads as read
524 self.message_check_and_set_read(cr, uid, ids, context=context)
525 # Sort and return the messages
526 messages = sorted(messages, key=lambda d: (-d['id']))
529 def message_get_pushed_messages(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
530 limit=100, offset=0, msg_search_domain=[], context=None):
531 """ OpenChatter: wall: get the pushed notifications and used them
532 to fetch messages to display on the wall.
534 :param fetch_ancestors: performs an ascended search; will add
535 to fetched msgs all their parents until
537 :param ancestor_ids: used when fetching ancestors
538 :param domain: domain to add to the search; especially child_of
539 is interesting when dealing with threaded display
540 :param ascent: performs an ascended search; will add to fetched msgs
541 all their parents until root_ids
542 :param root_ids: for ascent search
543 :return: list of mail.messages sorted by date
545 notification_obj = self.pool.get('mail.notification')
546 msg_obj = self.pool.get('mail.message')
547 # update message search
548 for arg in msg_search_domain:
549 if isinstance(arg, (tuple, list)):
550 arg[0] = 'message_id.' + arg[0]
551 # compose final domain
552 domain = [('user_id', '=', uid)] + msg_search_domain
554 notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
555 notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
556 msg_ids = [notification.message_id.id for notification in notifications]
558 msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
559 if (fetch_ancestors): msg_ids = self._message_search_ancestor_ids(cr, uid, ids, msg_ids, ancestor_ids, context=context)
560 msgs = msg_obj.read(cr, uid, msg_ids, context=context)
563 #------------------------------------------------------
565 #------------------------------------------------------
566 # message_process will call either message_new or message_update.
568 def message_process(self, cr, uid, model, message, custom_values=None,
569 save_original=False, strip_attachments=False,
571 """Process an incoming RFC2822 email message related to the
572 given thread model, relying on ``mail.message.parse()``
573 for the parsing operation, and then calling ``message_new``
574 (if the thread record did not exist) or ``message_update``
575 (if it did), then calling ``message_forward`` to automatically
576 notify other people that should receive this message.
578 :param string model: the thread model for which a new message
580 :param message: source of the RFC2822 mail
581 :type message: string or xmlrpclib.Binary
582 :type dict custom_values: optional dictionary of field values
583 to pass to ``message_new`` if a new
584 record needs to be created. Ignored
585 if the thread record already exists.
586 :param bool save_original: whether to keep a copy of the original
587 email source attached to the message after it is imported.
588 :param bool strip_attachments: whether to strip all attachments
589 before processing the message, in order to save some space.
591 # extract message bytes - we are forced to pass the message as binary because
592 # we don't know its encoding until we parse its headers and hence can't
593 # convert it to utf-8 for transport between the mailgate script and here.
594 if isinstance(message, xmlrpclib.Binary):
595 message = str(message.data)
597 model_pool = self.pool.get(model)
598 if self._name != model:
599 if context is None: context = {}
600 context.update({'thread_model': model})
602 mail_message = self.pool.get('mail.message')
605 # Warning: message_from_string doesn't always work correctly on unicode,
606 # we must use utf-8 strings here :-(
607 if isinstance(message, unicode):
608 message = message.encode('utf-8')
609 msg_txt = email.message_from_string(message)
610 msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context)
613 msg['state'] = 'received'
615 if strip_attachments and 'attachments' in msg:
616 del msg['attachments']
618 # Create New Record into particular model
619 def create_record(msg):
620 if hasattr(model_pool, 'message_new'):
621 return model_pool.message_new(cr, uid, msg,
625 if msg.get('references') or msg.get('in-reply-to'):
626 references = msg.get('references') or msg.get('in-reply-to')
627 if '\r\n' in references:
628 references = references.split('\r\n')
630 references = references.split(' ')
631 for ref in references:
633 res_id = tools.reference_re.search(ref)
635 res_id = res_id.group(1)
637 res_id = tools.res_re.search(msg['subject'])
639 res_id = res_id.group(1)
642 if model_pool.exists(cr, uid, res_id):
643 if hasattr(model_pool, 'message_update'):
644 model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
646 # referenced thread was not found, we'll have to create a new one
649 res_id = create_record(msg)
650 # To forward the email to other followers
651 self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
653 model_pool.message_mark_as_unread(cr, uid, [res_id], context=context)
656 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
657 """Called by ``message_process`` when a new message is received
658 for a given thread model, if the message did not belong to
660 The default behavior is to create a new record of the corresponding
661 model (based on some very basic info extracted from the message),
662 then attach the message to the newly created record
663 (by calling ``message_append_dict``).
664 Additional behavior may be implemented by overriding this method.
666 :param dict msg_dict: a map containing the email details and
667 attachments. See ``message_process`` and
668 ``mail.message.parse`` for details.
669 :param dict custom_values: optional dictionary of additional
670 field values to pass to create()
671 when creating the new thread record.
672 Be careful, these values may override
673 any other values coming from the message.
674 :param dict context: if a ``thread_model`` value is present
675 in the context, its value will be used
676 to determine the model of the record
677 to create (instead of the current model).
679 :return: the id of the newly created thread object
683 model = context.get('thread_model') or self._name
684 model_pool = self.pool.get(model)
685 fields = model_pool.fields_get(cr, uid, context=context)
686 data = model_pool.default_get(cr, uid, fields, context=context)
687 if 'name' in fields and not data.get('name'):
688 data['name'] = msg_dict.get('from', '')
689 if custom_values and isinstance(custom_values, dict):
690 data.update(custom_values)
691 res_id = model_pool.create(cr, uid, data, context=context)
692 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
695 def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
696 """ Called by ``message_process`` when a new message is received
697 for an existing thread. The default behavior is to create a
698 new mail.message in the given thread (by calling
699 ``message_append_dict``)
700 Additional behavior may be implemented by overriding this
703 :param dict msg_dict: a map containing the email details and
704 attachments. See ``message_process`` and
705 ``mail.message.parse()`` for details.
706 :param dict vals: a dict containing values to update records
707 given their ids; if the dict is None or is
708 void, no write operation is performed.
709 :param dict context: if a ``thread_model`` value is present
710 in the context, its value will be used
711 to determine the model of the thread to
712 update (instead of the current model).
715 self.write(cr, uid, ids, update_vals, context=context)
716 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
718 def message_thread_followers(self, cr, uid, ids, context=None):
719 """ Returns a list of email addresses of the people following
720 this thread, including the sender of each mail, and the
721 people who were in CC of the messages, if any.
724 if isinstance(ids, (str, int, long)):
726 for thread in self.browse(cr, uid, ids, context=context):
728 for message in thread.message_ids:
729 l.add((message.user_id and message.user_id.user_email) or '')
730 l.add(message.email_from or '')
731 l.add(message.email_cc or '')
732 res[thread.id] = filter(None, l)
735 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
736 """Sends an email to all people following the given threads.
737 The emails are forwarded immediately, not queued for sending,
740 :param str model: thread model
741 :param list thread_ids: ids of the thread records
742 :param msg: email.message.Message object to forward
743 :param email_error: optional email address to notify in case
744 of any delivery error during the forward.
747 model_pool = self.pool.get(model)
748 smtp_server_obj = self.pool.get('ir.mail_server')
749 mail_message = self.pool.get('mail.message')
750 for res in model_pool.browse(cr, uid, thread_ids, context=context):
751 if hasattr(model_pool, 'message_thread_followers'):
752 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
754 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
755 message_followers_emails = to_email(','.join(filter(None, followers)))
756 message_recipients = to_email(','.join(filter(None,
757 [decode(msg['from']),
759 decode(msg['cc'])])))
760 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
762 # TODO: we need an interface for this for all types of objects, not just leads
763 if hasattr(res, 'section_id'):
765 msg['reply-to'] = res.section_id.reply_to
767 smtp_from, = to_email(msg['from'])
768 msg['from'] = smtp_from
769 msg['to'] = ", ".join(forward_to)
770 msg['message-id'] = tools.generate_tracking_message_id(res.id)
771 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
772 subj = msg['subject']
773 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
774 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
775 msg['to'] = email_error
776 smtp_server_obj.send_email(cr, uid, msg)
779 def message_partner_by_email(self, cr, uid, email, context=None):
780 """Attempts to return the id of a partner address matching
781 the given ``email``, and the corresponding partner id.
782 Can be used by classes using the ``mail.thread`` mixin
783 to lookup the partner and use it in their implementation
784 of ``message_new`` to link the new record with a
785 corresponding partner.
786 The keys used in the returned dict are meant to map
787 to usual names for relationships towards a partner
788 and one of its addresses.
790 :param email: email address for which a partner
791 should be searched for.
793 :return: a map of the following form::
795 { 'partner_address_id': id or False,
796 'partner_id': pid or False }
798 partner_pool = self.pool.get('res.partner')
799 res = {'partner_id': False}
801 email = to_email(email)[0]
802 contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
804 contact = partner_pool.browse(cr, uid, contact_ids[0])
805 res['partner_id'] = contact.id
808 # for backwards-compatibility with old scripts
809 process_email = message_process
811 #------------------------------------------------------
813 #------------------------------------------------------
815 def log(self, cr, uid, id, message, secondary=False, context=None):
816 _logger.warning("log() is deprecated. As this module inherit from \
817 mail.thread, the message will be managed by this \
818 module instead of by the res.log mechanism. Please \
819 use the mail.thread OpenChatter API instead of the \
820 now deprecated res.log.")
821 self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
823 def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False,
824 type='notification', content_subtype='html', context=None):
825 if type in ['notification', 'comment']:
827 if content_subtype == 'html':
833 return self.message_append(cr, uid, ids, subject, body_html, body_text,
834 type, parent_id=parent_id,
835 content_subtype=content_subtype, context=context)
837 #------------------------------------------------------
838 # Subscription mechanism
839 #------------------------------------------------------
841 def message_get_subscribers(self, cr, uid, ids, context=None):
842 """ Returns the current document followers. Basically this method
843 checks in mail.subscription for entries with matching res_model,
845 This method can be overriden to add implicit subscribers, such
846 as project managers, by adding their user_id to the list of
847 ids returned by this method.
849 subscr_obj = self.pool.get('mail.subscription')
850 subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
851 return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
853 def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
854 """ Returns the current document followers as a read result. Used
855 mainly for Chatter having only one method to call to have
858 user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
859 return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
861 def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
862 """ Check if uid or user_id (if set) is a subscriber to the current
865 :param user_id: if set, check is done on user_id; if not set
868 sub_user_id = uid if user_id is None else user_id
869 if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
873 def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
874 """ Subscribe the user (or user_ids) to the current document.
876 :param user_ids: a list of user_ids; if not set, subscribe
879 subscription_obj = self.pool.get('mail.subscription')
880 to_subscribe_uids = [uid] if user_ids is None else user_ids
883 already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
884 for user_id in to_subscribe_uids:
885 if user_id in already_subscribed_user_ids: continue
886 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
889 def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
890 """ Unsubscribe the user (or user_ids) from the current document.
892 :param user_ids: a list of user_ids; if not set, subscribe
895 # Trying to unsubscribe somebody not in subscribers: returns False
896 # if special management is needed; allows to know that an automatically
897 # subscribed user tries to unsubscribe and allows to warn him
898 to_unsubscribe_uids = [uid] if user_ids is None else user_ids
899 subscription_obj = self.pool.get('mail.subscription')
900 to_delete_sub_ids = subscription_obj.search(cr, uid,
901 ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
902 if not to_delete_sub_ids:
904 return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
906 #------------------------------------------------------
908 #------------------------------------------------------
910 def message_create_notify_by_email(self, cr, uid, new_msg_values, user_to_notify_ids, context=None):
911 """ When creating a new message and pushing notifications, emails
912 must be send if users have chosen to receive notifications
913 by email via the notification_email_pref field.
915 ``notification_email_pref`` can have 3 values :
916 - all: receive all notification by email (for example for shared
918 - to_me: messages send directly to me (@login, messages on res.users)
919 - never: never receive notifications
920 Note that an user should never receive notifications for messages
923 :param new_msg_values: dictionary of message values, those that
924 are given to the create method
925 :param user_to_notify_ids: list of user_ids, user that will
926 receive a notification on their Wall
928 message_obj = self.pool.get('mail.message')
929 res_users_obj = self.pool.get('res.users')
930 body = new_msg_values.get('body_html', '') if new_msg_values.get('content_subtype') == 'html' else new_msg_values.get('body_text', '')
932 # remove message writer
933 if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0:
934 user_to_notify_ids.remove(new_msg_values.get('user_id'))
936 # get user_ids directly asked
937 user_to_push_from_parse_ids = self.message_parse_users(cr, uid, body, context=context)
939 # try to find an email_to
941 for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context):
942 if not user.notification_email_pref == 'all' and \
943 not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
945 if not user.user_email:
947 email_to = '%s, %s' % (email_to, user.user_email)
948 email_to = email_to.lstrip(', ')
950 # did not find any email address: not necessary to create an email
954 # try to find an email_from
955 current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
956 email_from = new_msg_values.get('email_from')
958 email_from = current_user.user_email
960 # get email content, create it (with mail_message.create)
961 email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
962 email_id = message_obj.create(cr, uid, email_values, context=context)
965 def message_create_notify_get_email_dict(self, cr, uid, new_msg_values, email_from, email_to, context=None):
966 values = dict(new_msg_values)
968 body_html = new_msg_values.get('body_html', '')
970 body_html += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
971 body_text = new_msg_values.get('body_text', '')
973 body_text += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
977 'email_from': email_from,
978 'email_to': email_to,
979 'subject': 'New message',
980 'content_subtype': new_msg_values.get('content_subtype', 'plain'),
981 'body_html': body_html,
982 'body_text': body_text,
989 def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
990 notif_obj = self.pool.get('mail.notification')
991 msg_obj = self.pool.get('mail.message')
993 notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
995 notif_msg_ids = msg_ids
996 to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
997 return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
999 #------------------------------------------------------
1001 #------------------------------------------------------
1003 def message_create_set_unread(self, cr, uid, ids, context=None):
1004 """ When creating a new message, set as unread if uid is not the
1005 object responsible. """
1006 for obj in self.browse(cr, uid, ids, context=context):
1007 if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
1008 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1010 def message_check_and_set_unread(self, cr, uid, ids, context=None):
1011 """ Set unread if uid is the object responsible or if the object has
1013 for obj in self.browse(cr, uid, ids, context=context):
1014 if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
1015 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1017 def message_mark_as_unread(self, cr, uid, ids, context=None):
1018 """ Set as unread. """
1019 return self.write(cr, uid, ids, {'message_state': False}, context=context)
1021 def message_check_and_set_read(self, cr, uid, ids, context=None):
1022 """ Set read if uid is the object responsible. """
1023 for obj in self.browse(cr, uid, ids, context=context):
1024 if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
1025 self.message_mark_as_read(cr, uid, [obj.id], context=context)
1027 def message_mark_as_read(self, cr, uid, ids, context=None):
1028 """ Set as read. """
1029 return self.write(cr, uid, ids, {'message_state': True}, context=context)
1032 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: