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_load_ids(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 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 subscription_obj = self.pool.get('mail.subscription')
154 notification_obj = self.pool.get('mail.notification')
155 res_users_obj = self.pool.get('res.users')
156 body = vals.get('body_html', '') if vals.get('subtype', 'plain') == 'html' else vals.get('body_text', '')
158 # automatically subscribe the writer of the message
160 self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
162 # get users that will get a notification pushed
163 user_to_push_ids = self.message_create_get_notification_user_ids(cr, uid, [thread_id], vals, context=context)
164 user_to_push_from_parse_ids = self.message_parse_users(cr, uid, [thread_id], body, context=context)
166 # set email_from and email_to for comments and notifications
167 if vals.get('type', False) and vals['type'] == 'comment' or vals['type'] == 'notification':
168 current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
169 if not vals.get('email_from', False):
170 vals['email_from'] = current_user.user_email
171 if not vals.get('email_to', False):
173 for user in res_users_obj.browse(cr, uid, user_to_push_ids, context=context):
174 if not user.notification_email_pref == 'all' and \
175 not (user.notification_email_pref == 'comments' and vals['type'] == 'comment') and \
176 not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
178 if not user.user_email:
180 email_to = '%s, %s' % (email_to, user.user_email)
181 email_to = email_to.lstrip(', ')
183 vals['email_to'] = email_to
184 vals['state'] = 'outgoing'
187 msg_id = message_obj.create(cr, uid, vals, context=context)
189 # Set as unread if writer is not the document responsible
190 self.message_create_set_unread(cr, uid, [thread_id], context=context)
192 # special: if install mode, do not push demo data
193 if context.get('install_mode', False):
197 for id in user_to_push_ids:
198 notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
202 def message_create_get_notification_user_ids(self, cr, uid, thread_ids, new_msg_vals, context=None):
207 body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
208 for thread_id in thread_ids:
210 notif_user_ids += self.message_get_subscribers(cr, uid, [thread_id], context=context)
211 # add users requested via parsing message (@login)
212 notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
213 # add users requested to perform an action (need_action mechanism)
214 if hasattr(self, 'get_needaction_user_ids'):
215 notif_user_ids += self.get_needaction_user_ids(cr, uid, [thread_id], context=context)[thread_id]
216 # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
217 if new_msg_vals.get('parent_id'):
218 notif_obj = self.pool.get('mail.notification')
219 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
220 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
221 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
223 # remove duplicate entries
224 notif_user_ids = list(set(notif_user_ids))
225 return notif_user_ids
227 def message_parse_users(self, cr, uid, ids, string, context=None):
228 """Parse message content
229 - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
230 this supports login that are emails (such as @admin@lapin.net)
232 regex = re.compile('(^|\s)@((\w|@|\.)*)')
233 login_lst = [item[1] for item in regex.findall(string)]
234 if not login_lst: return []
235 user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
238 #------------------------------------------------------
239 # Generic message api
240 #------------------------------------------------------
242 def message_capable_models(self, cr, uid, context=None):
244 for model_name in self.pool.obj_list():
245 model = self.pool.get(model_name)
246 if 'mail.thread' in getattr(model, '_inherit', []):
247 ret_dict[model_name] = model._description
250 def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
251 parent_id=False, type='email', subtype='plain', state='received',
252 email_to=False, email_from=False, email_cc=None, email_bcc=None,
253 reply_to=None, email_date=None, message_id=False, references=None,
254 attachments=None, headers=None, original=None, context=None):
255 """Creates a new mail.message attached to the current mail.thread,
256 containing all the details passed as parameters. All attachments
257 will be attached to the thread record as well as to the actual
259 If ``email_from`` is not set or ``type`` not set as 'email',
260 a note message is created, without the usual envelope
261 attributes (sender, recipients, etc.).
262 The creation of the message is done by calling ``message_create``
263 method, that will manage automatic pushing of notifications.
265 :param threads: list of thread ids, or list of browse_records representing
266 threads to which a new message should be attached
267 :param subject: subject of the message, or description of the event if this
268 is an *event log* entry.
269 :param body_text: plaintext contents of the mail or log message
270 :param body_html: html contents of the mail or log message
271 :param parent_id: id of the parent message (threaded messaging model)
272 :param type: optional type of message: 'email', 'comment', 'notification'
273 :param subtype: optional subtype of message: 'plain' or 'html', corresponding to the main
274 body contents (body_text or body_html).
275 :param state: optional state of message; 'received' by default
276 :param email_to: Email-To / Recipient address
277 :param email_from: Email From / Sender address if any
278 :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
279 :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
280 :param reply_to: reply_to header
281 :param email_date: email date string if different from now, in server timezone
282 :param message_id: optional email identifier
283 :param references: optional email references
284 :param headers: mail headers to store
285 :param dict attachments: map of attachment filenames to binary contents, if any.
286 :param str original: optional full source of the RFC2822 email, for reference
287 :param dict context: if a ``thread_model`` value is present
288 in the context, its value will be used
289 to determine the model of the thread to
290 update (instead of the current model).
294 if attachments is None:
298 edate = parsedate(email_date)
299 if edate is not None:
300 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
302 if all(isinstance(thread_id, (int, long)) for thread_id in threads):
303 model = context.get('thread_model') or self._name
304 model_pool = self.pool.get(model)
305 threads = model_pool.browse(cr, uid, threads, context=context)
307 ir_attachment = self.pool.get('ir.attachment')
308 mail_message = self.pool.get('mail.message')
311 for thread in threads:
313 for attachment in attachments:
314 fname, fcontent = attachment
315 if isinstance(fcontent, unicode):
316 fcontent = fcontent.encode('utf-8')
319 'datas': base64.b64encode(str(fcontent)),
320 'datas_fname': fname,
321 'description': _('Mail attachment'),
322 'res_model': thread._name,
325 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
327 partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
328 if not partner_id and thread._name == 'res.partner':
329 partner_id = thread.id
332 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
333 'body_html': body_html or '',
334 'parent_id': parent_id,
335 'date': email_date or fields.datetime.now(),
339 'message_id': message_id,
340 'attachment_ids': [(6, 0, to_attach)],
342 'model' : thread._name,
344 'partner_id': partner_id,
347 if email_from or type == 'email':
348 for param in (email_to, email_cc, email_bcc):
349 if isinstance(param, list):
350 param = ", ".join(param)
352 'subject': subject or _('History'),
353 'email_to': email_to,
354 'email_from': email_from or \
355 (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
356 'email_cc': email_cc,
357 'email_bcc': email_bcc,
358 'references': references,
360 'reply_to': reply_to,
361 'original': original, })
363 new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
366 def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
367 """Creates a new mail.message attached to the given threads (``ids``),
368 with the contents of ``msg_dict``, by calling ``message_append``
369 with the mail details. All attachments in msg_dict will be
370 attached to the object record as well as to the actual
373 :param dict msg_dict: a map containing the email details and
374 attachments. See ``message_process()`` and
375 ``mail.message.parse()`` for details on
377 :param dict context: if a ``thread_model`` value is present
378 in the context, its value will be used
379 to determine the model of the thread to
380 update (instead of the current model).
382 return self.message_append(cr, uid, ids,
383 subject = msg_dict.get('subject'),
384 body_text = msg_dict.get('body_text'),
385 body_html= msg_dict.get('body_html'),
386 parent_id = msg_dict.get('parent_id', False),
387 type = msg_dict.get('type', 'email'),
388 subtype = msg_dict.get('subtype', 'plain'),
389 state = msg_dict.get('state', 'received'),
390 email_from = msg_dict.get('from', msg_dict.get('email_from')),
391 email_to = msg_dict.get('to', msg_dict.get('email_to')),
392 email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
393 email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
394 reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
395 email_date = msg_dict.get('date'),
396 message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
397 references = msg_dict.get('references')\
398 or msg_dict.get('in-reply-to'),
399 attachments = msg_dict.get('attachments'),
400 headers = msg_dict.get('headers'),
401 original = msg_dict.get('original'),
404 def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
405 """ Given message child_ids
406 Find their ancestors until root ids"""
409 msg_obj = self.pool.get('mail.message')
410 tmp_msgs = msg_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
411 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]
412 child_ids += parent_ids
413 cur_iter = 0; max_iter = 100; # avoid infinite loop
414 while (parent_ids and (cur_iter < max_iter)):
416 tmp_msgs = msg_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
417 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]
418 child_ids += parent_ids
419 if (cur_iter > max_iter):
420 _logger.warning("Possible infinite loop in _message_add_ancestor_ids. Note that this algorithm is intended to check for cycle in message graph.")
423 def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
424 """ OpenChatter feature: return thread messages ids. It searches in
425 mail.messages where res_id = ids, (res_)model = current model.
426 :param domain: domain to add to the search; especially child_of
427 is interesting when dealing with threaded display
428 :param ascent: performs an ascended search; will add to fetched msgs
429 all their parents until root_ids
430 :param root_ids: for ascent search
431 :param root_ids: root_ids when performing an ascended search
435 msg_obj = self.pool.get('mail.message')
436 msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', ids), ('model', '=', self._name)] + domain,
437 limit=limit, offset=offset, context=context)
438 if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
441 def message_load(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
442 """ OpenChatter feature: return thread messages
444 msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
445 msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
448 self.message_check_and_set_read(cr, uid, ids, context=context)
450 """ Retrieve all attachments names """
454 for attach_id in msg["attachment_ids"]:
455 map_id_to_name[attach_id] = '' # use empty string as a placeholder
457 ids = map_id_to_name.keys()
458 names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
460 # convert the list of tuples into a dictionnary
462 map_id_to_name[name[0]] = name[1]
464 # give corresponding ids and names to each message
466 msg["attachments"] = []
468 for attach_id in msg["attachment_ids"]:
469 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
471 """ Sort and return messages """
472 msgs = sorted(msgs, key=lambda d: (-d['id']))
475 def get_pushed_messages(self, cr, uid, ids, limit=100, offset=0, msg_search_domain=[], ascent=False, root_ids=[], context=None):
476 """ OpenChatter: wall: get messages to display (=pushed notifications)
477 :param domain: domain to add to the search; especially child_of
478 is interesting when dealing with threaded display
479 :param ascent: performs an ascended search; will add to fetched msgs
480 all their parents until root_ids
481 :param root_ids: for ascent search
482 :return list of mail.messages sorted by date
484 if context is None: context = {}
485 notification_obj = self.pool.get('mail.notification')
486 msg_obj = self.pool.get('mail.message')
487 # update message search
488 for arg in msg_search_domain:
489 if isinstance(arg, (tuple, list)):
490 arg[0] = 'message_id.' + arg[0]
491 # compose final domain
492 domain = [('user_id', '=', uid)] + msg_search_domain
494 notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
495 notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
496 msg_ids = [notification.message_id.id for notification in notifications]
498 msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
499 if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
500 msgs = msg_obj.read(cr, uid, msg_ids, context=context)
503 #------------------------------------------------------
505 #------------------------------------------------------
506 # message_process will call either message_new or message_update.
508 def message_process(self, cr, uid, model, message, custom_values=None,
509 save_original=False, strip_attachments=False,
511 """Process an incoming RFC2822 email message related to the
512 given thread model, relying on ``mail.message.parse()``
513 for the parsing operation, and then calling ``message_new``
514 (if the thread record did not exist) or ``message_update``
515 (if it did), then calling ``message_forward`` to automatically
516 notify other people that should receive this message.
518 :param string model: the thread model for which a new message
520 :param message: source of the RFC2822 mail
521 :type message: string or xmlrpclib.Binary
522 :type dict custom_values: optional dictionary of field values
523 to pass to ``message_new`` if a new
524 record needs to be created. Ignored
525 if the thread record already exists.
526 :param bool save_original: whether to keep a copy of the original
527 email source attached to the message after it is imported.
528 :param bool strip_attachments: whether to strip all attachments
529 before processing the message, in order to save some space.
531 # extract message bytes - we are forced to pass the message as binary because
532 # we don't know its encoding until we parse its headers and hence can't
533 # convert it to utf-8 for transport between the mailgate script and here.
534 if isinstance(message, xmlrpclib.Binary):
535 message = str(message.data)
537 model_pool = self.pool.get(model)
538 if self._name != model:
539 if context is None: context = {}
540 context.update({'thread_model': model})
542 mail_message = self.pool.get('mail.message')
546 # Warning: message_from_string doesn't always work correctly on unicode,
547 # we must use utf-8 strings here :-(
548 if isinstance(message, unicode):
549 message = message.encode('utf-8')
550 msg_txt = email.message_from_string(message)
551 msg = mail_message.parse_message(msg_txt, save_original=save_original)
553 if strip_attachments and 'attachments' in msg:
554 del msg['attachments']
556 # Create New Record into particular model
557 def create_record(msg):
558 if hasattr(model_pool, 'message_new'):
559 return model_pool.message_new(cr, uid, msg,
563 if msg.get('references') or msg.get('in-reply-to'):
564 references = msg.get('references') or msg.get('in-reply-to')
565 if '\r\n' in references:
566 references = references.split('\r\n')
568 references = references.split(' ')
569 for ref in references:
571 res_id = tools.reference_re.search(ref)
573 res_id = res_id.group(1)
575 res_id = tools.res_re.search(msg['subject'])
577 res_id = res_id.group(1)
580 if model_pool.exists(cr, uid, res_id):
581 if hasattr(model_pool, 'message_update'):
582 model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
584 # referenced thread was not found, we'll have to create a new one
587 res_id = create_record(msg)
588 # To forward the email to other followers
589 self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
592 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
593 """Called by ``message_process`` when a new message is received
594 for a given thread model, if the message did not belong to
596 The default behavior is to create a new record of the corresponding
597 model (based on some very basic info extracted from the message),
598 then attach the message to the newly created record
599 (by calling ``message_append_dict``).
600 Additional behavior may be implemented by overriding this method.
602 :param dict msg_dict: a map containing the email details and
603 attachments. See ``message_process`` and
604 ``mail.message.parse`` for details.
605 :param dict custom_values: optional dictionary of additional
606 field values to pass to create()
607 when creating the new thread record.
608 Be careful, these values may override
609 any other values coming from the message.
610 :param dict context: if a ``thread_model`` value is present
611 in the context, its value will be used
612 to determine the model of the record
613 to create (instead of the current model).
615 :return: the id of the newly created thread object
619 model = context.get('thread_model') or self._name
620 model_pool = self.pool.get(model)
621 fields = model_pool.fields_get(cr, uid, context=context)
622 data = model_pool.default_get(cr, uid, fields, context=context)
623 if 'name' in fields and not data.get('name'):
624 data['name'] = msg_dict.get('from', '')
625 if custom_values and isinstance(custom_values, dict):
626 data.update(custom_values)
627 res_id = model_pool.create(cr, uid, data, context=context)
628 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
631 def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
632 """ Called by ``message_process`` when a new message is received
633 for an existing thread. The default behavior is to create a
634 new mail.message in the given thread (by calling
635 ``message_append_dict``)
636 Additional behavior may be implemented by overriding this
639 :param dict msg_dict: a map containing the email details and
640 attachments. See ``message_process`` and
641 ``mail.message.parse()`` for details.
642 :param dict vals: a dict containing values to update records
643 given their ids; if the dict is None or is
644 void, no write operation is performed.
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).
651 self.write(cr, uid, ids, update_vals, context=context)
652 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
654 def message_thread_followers(self, cr, uid, ids, context=None):
655 """Returns a list of email addresses of the people following
656 this thread, including the sender of each mail, and the
657 people who were in CC of the messages, if any.
660 if isinstance(ids, (str, int, long)):
662 for thread in self.browse(cr, uid, ids, context=context):
664 for message in thread.message_ids:
665 l.add((message.user_id and message.user_id.user_email) or '')
666 l.add(message.email_from or '')
667 l.add(message.email_cc or '')
668 res[thread.id] = filter(None, l)
671 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
672 """Sends an email to all people following the given threads.
673 The emails are forwarded immediately, not queued for sending,
676 :param str model: thread model
677 :param list thread_ids: ids of the thread records
678 :param msg: email.message.Message object to forward
679 :param email_error: optional email address to notify in case
680 of any delivery error during the forward.
683 model_pool = self.pool.get(model)
684 smtp_server_obj = self.pool.get('ir.mail_server')
685 mail_message = self.pool.get('mail.message')
686 for res in model_pool.browse(cr, uid, thread_ids, context=context):
687 if hasattr(model_pool, 'message_thread_followers'):
688 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
690 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
691 message_followers_emails = to_email(','.join(filter(None, followers)))
692 message_recipients = to_email(','.join(filter(None,
693 [decode(msg['from']),
695 decode(msg['cc'])])))
696 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
698 # TODO: we need an interface for this for all types of objects, not just leads
699 if hasattr(res, 'section_id'):
701 msg['reply-to'] = res.section_id.reply_to
703 smtp_from, = to_email(msg['from'])
704 msg['from'] = smtp_from
705 msg['to'] = ", ".join(forward_to)
706 msg['message-id'] = tools.generate_tracking_message_id(res.id)
707 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
708 subj = msg['subject']
709 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
710 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
711 msg['to'] = email_error
712 smtp_server_obj.send_email(cr, uid, msg)
715 def message_partner_by_email(self, cr, uid, email, context=None):
716 """Attempts to return the id of a partner address matching
717 the given ``email``, and the corresponding partner id.
718 Can be used by classes using the ``mail.thread`` mixin
719 to lookup the partner and use it in their implementation
720 of ``message_new`` to link the new record with a
721 corresponding partner.
722 The keys used in the returned dict are meant to map
723 to usual names for relationships towards a partner
724 and one of its addresses.
726 :param email: email address for which a partner
727 should be searched for.
729 :return: a map of the following form::
731 { 'partner_address_id': id or False,
732 'partner_id': pid or False }
734 partner_pool = self.pool.get('res.partner')
735 res = {'partner_id': False}
737 email = to_email(email)[0]
738 contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
740 contact = partner_pool.browse(cr, uid, contact_ids[0])
741 res['partner_id'] = contact.id
744 # for backwards-compatibility with old scripts
745 process_email = message_process
747 #------------------------------------------------------
749 #------------------------------------------------------
751 def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
754 notification_obj = self.pool.get('mail.notification')
756 msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, subtype=subtype, context=context)
757 # escape if in install mode or note writing was not successfull
758 if 'install_mode' in context:
760 if not isinstance(msg_ids, (list)):
762 # get already existing notigications
763 notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
764 already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
765 # get base.group_user group
766 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
767 group_id = res and res[1] or False
768 if not group_id: return True
769 group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
770 for user in group.users:
771 if user.id in already_pushed_user_ids: continue
772 for msg_id in msg_ids:
773 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
776 def log(self, cr, uid, id, message, secondary=False, context=None):
777 _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
778 self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
780 def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
781 if type in ['notification', 'reply']:
783 if subtype == 'html':
789 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)
791 #------------------------------------------------------
792 # Subscription mechanism
793 #------------------------------------------------------
795 def message_get_subscribers(self, cr, uid, ids, context=None):
796 """ Returns the current document followers. Basically this method
797 checks in mail.subscription for entries with matching res_model,
800 :param get_ids: if set to True, return the ids of users; if set
801 to False, returns the result of a read in res.users
803 subscr_obj = self.pool.get('mail.subscription')
804 subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
805 return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
807 def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None):
808 """ Returns the current document followers as a read result. Used
809 mainly for Chatter having only one method to call to have
812 user_ids = self.message_get_subscribers(cr, uid, ids, context=context)
813 return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context)
815 def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
816 """ Check if uid or user_id (if set) is a subscriber to the current
819 :param user_id: if set, check is done on user_id; if not set
822 sub_user_id = uid if user_id is None else user_id
823 if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context):
827 def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
828 """ Subscribe the user (or user_ids) to the current document.
830 :param user_ids: a list of user_ids; if not set, subscribe
833 subscription_obj = self.pool.get('mail.subscription')
834 to_subscribe_uids = [uid] if user_ids is None else user_ids
837 already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context)
838 for user_id in to_subscribe_uids:
839 if user_id in already_subscribed_user_ids: continue
840 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
843 def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
844 """ Unsubscribe the user (or user_ids) from the current document.
846 :param user_ids: a list of user_ids; if not set, subscribe
849 # Trying to unsubscribe somebody not in subscribers: returns False
850 # if special management is needed; allows to know that an automatically
851 # subscribed user tries to unsubscribe and allows to warn him
852 mail_thread_model = self.pool.get('mail.thread')
853 if not user_ids and not uid in mail_thread_model.message_get_subscribers(cr, uid, ids, context=context):
855 subscription_obj = self.pool.get('mail.subscription')
856 to_unsubscribe_uids = [uid] if user_ids is None else user_ids
857 to_delete_sub_ids = subscription_obj.search(cr, uid,
858 ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
859 return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
861 #------------------------------------------------------
863 #------------------------------------------------------
865 def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
866 notif_obj = self.pool.get('mail.notification')
867 msg_obj = self.pool.get('mail.message')
869 notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
871 notif_msg_ids = msg_ids
872 to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
873 return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
875 #------------------------------------------------------
877 #------------------------------------------------------
879 def message_create_set_unread(self, cr, uid, ids, context=None):
880 """ When creating a new message, set as unread if uid is not the
881 object responsible. """
882 for obj in self.browse(cr, uid, ids, context=context):
883 if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid):
884 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
886 def message_check_and_set_unread(self, cr, uid, ids, context=None):
887 """ Set unread if uid is the object responsible or if the object has
889 for obj in self.browse(cr, uid, ids, context=context):
890 if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid):
891 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
893 def message_mark_as_unread(self, cr, uid, ids, context=None):
894 """ Set as unread. """
895 return self.write(cr, uid, ids, {'message_state': False}, context=context)
897 def message_check_and_set_read(self, cr, uid, ids, context=None):
898 """ Set read if uid is the object responsible. """
899 for obj in self.browse(cr, uid, ids, context=context):
900 if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid:
901 self.message_mark_as_read(cr, uid, [obj.id], context=context)
903 def message_mark_as_read(self, cr, uid, ids, context=None):
905 return self.write(cr, uid, ids, {'message_state': True}, context=context)
908 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: