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