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_social is a dummy field that should not be used
73 'message_ids_social': fields.function(_get_message_ids, method=True,
74 type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'),
75 'message_state': fields.selection([('read', 'Read'),('unread', 'Unread')], 'Message State'),
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 return self.pool.get('mail.message').read(cr, uid, msg_ids, context=context)
419 def get_pushed_messages(self, cr, uid, ids, limit=100, offset=0, msg_search_domain=[], ascent=False, root_ids=[], context=None):
420 """ OpenChatter: wall: get messages to display (=pushed notifications)
421 :param domain: domain to add to the search; especially child_of
422 is interesting when dealing with threaded display
423 :param ascent: performs an ascended search; will add to fetched msgs
424 all their parents until root_ids
425 :param root_ids: for ascent search
426 :return list of mail.messages sorted by date
428 if context is None: context = {}
429 notification_obj = self.pool.get('mail.notification')
430 msg_obj = self.pool.get('mail.message')
431 # update message search
432 for arg in msg_search_domain:
433 if isinstance(arg, (tuple, list)):
434 arg[0] = 'message_id.' + arg[0]
435 # compose final domain
436 domain = [('user_id', '=', uid)] + msg_search_domain
438 notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
439 notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
440 msg_ids = [notification.message_id.id for notification in notifications]
442 msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
443 if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
444 msgs = msg_obj.read(cr, uid, msg_ids, context=context)
447 #------------------------------------------------------
449 #------------------------------------------------------
450 # message_process will call either message_new or message_update.
452 def message_process(self, cr, uid, model, message, custom_values=None,
453 save_original=False, strip_attachments=False,
455 """Process an incoming RFC2822 email message related to the
456 given thread model, relying on ``mail.message.parse()``
457 for the parsing operation, and then calling ``message_new``
458 (if the thread record did not exist) or ``message_update``
459 (if it did), then calling ``message_forward`` to automatically
460 notify other people that should receive this message.
462 :param string model: the thread model for which a new message
464 :param message: source of the RFC2822 mail
465 :type message: string or xmlrpclib.Binary
466 :type dict custom_values: optional dictionary of field values
467 to pass to ``message_new`` if a new
468 record needs to be created. Ignored
469 if the thread record already exists.
470 :param bool save_original: whether to keep a copy of the original
471 email source attached to the message after it is imported.
472 :param bool strip_attachments: whether to strip all attachments
473 before processing the message, in order to save some space.
475 # extract message bytes - we are forced to pass the message as binary because
476 # we don't know its encoding until we parse its headers and hence can't
477 # convert it to utf-8 for transport between the mailgate script and here.
478 if isinstance(message, xmlrpclib.Binary):
479 message = str(message.data)
481 model_pool = self.pool.get(model)
482 if self._name != model:
483 if context is None: context = {}
484 context.update({'thread_model': model})
486 mail_message = self.pool.get('mail.message')
490 # Warning: message_from_string doesn't always work correctly on unicode,
491 # we must use utf-8 strings here :-(
492 if isinstance(message, unicode):
493 message = message.encode('utf-8')
494 msg_txt = email.message_from_string(message)
495 msg = mail_message.parse_message(msg_txt, save_original=save_original)
497 if strip_attachments and 'attachments' in msg:
498 del msg['attachments']
500 # Create New Record into particular model
501 def create_record(msg):
502 if hasattr(model_pool, 'message_new'):
503 return model_pool.message_new(cr, uid, msg,
507 if msg.get('references') or msg.get('in-reply-to'):
508 references = msg.get('references') or msg.get('in-reply-to')
509 if '\r\n' in references:
510 references = references.split('\r\n')
512 references = references.split(' ')
513 for ref in references:
515 res_id = tools.reference_re.search(ref)
517 res_id = res_id.group(1)
519 res_id = tools.res_re.search(msg['subject'])
521 res_id = res_id.group(1)
524 if model_pool.exists(cr, uid, res_id):
525 if hasattr(model_pool, 'message_update'):
526 model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
528 # referenced thread was not found, we'll have to create a new one
531 res_id = create_record(msg)
532 #To forward the email to other followers
533 self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
536 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
537 """Called by ``message_process`` when a new message is received
538 for a given thread model, if the message did not belong to
540 The default behavior is to create a new record of the corresponding
541 model (based on some very basic info extracted from the message),
542 then attach the message to the newly created record
543 (by calling ``message_append_dict``).
544 Additional behavior may be implemented by overriding this method.
546 :param dict msg_dict: a map containing the email details and
547 attachments. See ``message_process`` and
548 ``mail.message.parse`` for details.
549 :param dict custom_values: optional dictionary of additional
550 field values to pass to create()
551 when creating the new thread record.
552 Be careful, these values may override
553 any other values coming from the message.
554 :param dict context: if a ``thread_model`` value is present
555 in the context, its value will be used
556 to determine the model of the record
557 to create (instead of the current model).
559 :return: the id of the newly created thread object
563 model = context.get('thread_model') or self._name
564 model_pool = self.pool.get(model)
565 fields = model_pool.fields_get(cr, uid, context=context)
566 data = model_pool.default_get(cr, uid, fields, context=context)
567 if 'name' in fields and not data.get('name'):
568 data['name'] = msg_dict.get('from','')
569 if custom_values and isinstance(custom_values, dict):
570 data.update(custom_values)
571 res_id = model_pool.create(cr, uid, data, context=context)
572 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
573 self.write(cr, uid, [res_id], {'message_state':'unread'}, context)
576 def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
577 """Called by ``message_process`` when a new message is received
578 for an existing thread. The default behavior is to create a
579 new mail.message in the given thread (by calling
580 ``message_append_dict``)
581 Additional behavior may be implemented by overriding this
584 :param dict msg_dict: a map containing the email details and
585 attachments. See ``message_process`` and
586 ``mail.message.parse()`` for details.
587 :param dict context: if a ``thread_model`` value is present
588 in the context, its value will be used
589 to determine the model of the thread to
590 update (instead of the current model).
592 self.write(cr, uid, ids, {'message_state':'unread'}, context=context)
593 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
595 def message_thread_followers(self, cr, uid, ids, context=None):
596 """Returns a list of email addresses of the people following
597 this thread, including the sender of each mail, and the
598 people who were in CC of the messages, if any.
601 if isinstance(ids, (str, int, long)):
603 for thread in self.browse(cr, uid, ids, context=context):
605 for message in thread.message_ids:
606 l.add((message.user_id and message.user_id.user_email) or '')
607 l.add(message.email_from or '')
608 l.add(message.email_cc or '')
609 res[thread.id] = filter(None, l)
612 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
613 """Sends an email to all people following the given threads.
614 The emails are forwarded immediately, not queued for sending,
617 :param str model: thread model
618 :param list thread_ids: ids of the thread records
619 :param msg: email.message.Message object to forward
620 :param email_error: optional email address to notify in case
621 of any delivery error during the forward.
624 model_pool = self.pool.get(model)
625 smtp_server_obj = self.pool.get('ir.mail_server')
626 mail_message = self.pool.get('mail.message')
627 for res in model_pool.browse(cr, uid, thread_ids, context=context):
628 if hasattr(model_pool, 'message_thread_followers'):
629 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
631 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
632 message_followers_emails = to_email(','.join(filter(None, followers)))
633 message_recipients = to_email(','.join(filter(None,
634 [decode(msg['from']),
636 decode(msg['cc'])])))
637 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
639 # TODO: we need an interface for this for all types of objects, not just leads
640 if hasattr(res, 'section_id'):
642 msg['reply-to'] = res.section_id.reply_to
644 smtp_from, = to_email(msg['from'])
645 msg['from'] = smtp_from
646 msg['to'] = ", ".join(forward_to)
647 msg['message-id'] = tools.generate_tracking_message_id(res.id)
648 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
649 subj = msg['subject']
650 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
651 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
652 msg['to'] = email_error
653 smtp_server_obj.send_email(cr, uid, msg)
656 def message_partner_by_email(self, cr, uid, email, context=None):
657 """Attempts to return the id of a partner address matching
658 the given ``email``, and the corresponding partner id.
659 Can be used by classes using the ``mail.thread`` mixin
660 to lookup the partner and use it in their implementation
661 of ``message_new`` to link the new record with a
662 corresponding partner.
663 The keys used in the returned dict are meant to map
664 to usual names for relationships towards a partner
665 and one of its addresses.
667 :param email: email address for which a partner
668 should be searched for.
670 :return: a map of the following form::
672 { 'partner_address_id': id or False,
673 'partner_id': pid or False }
675 partner_pool = self.pool.get('res.partner')
676 res = {'partner_id': False}
678 email = to_email(email)[0]
679 contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
681 contact = partner_pool.browse(cr, uid, contact_ids[0])
682 res['partner_id'] = contact.id
685 # for backwards-compatibility with old scripts
686 process_email = message_process
688 #------------------------------------------------------
690 #------------------------------------------------------
692 def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
695 notification_obj = self.pool.get('mail.notification')
697 msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, subtype=subtype, context=context)
698 # escape if in install mode or note writing was not successfull
699 if 'install_mode' in context:
701 if not isinstance(msg_ids, (list)):
703 # get already existing notigications
704 notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
705 already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
706 # get base.group_user group
707 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
708 group_id = res and res[1] or False
709 if not group_id: return True
710 group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
711 for user in group.users:
712 if user.id in already_pushed_user_ids: continue
713 for msg_id in msg_ids:
714 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
717 def log(self, cr, uid, id, message, secondary=False, context=None):
718 _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
719 self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
721 def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
723 if type == 'notification':
724 subject = _('System notification')
725 elif type == 'comment' and not parent_id:
726 subject = _('Comment')
727 elif type == 'comment' and parent_id:
729 if subtype == 'html':
735 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)
737 #------------------------------------------------------
738 # Subscription mechanism
739 #------------------------------------------------------
741 def message_get_subscribers_ids(self, cr, uid, ids, context=None):
742 subscr_obj = self.pool.get('mail.subscription')
743 subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
744 subs = subscr_obj.read(cr, uid, subscr_ids, context=context)
745 return [sub['user_id'][0] for sub in subs]
747 def message_get_subscribers(self, cr, uid, ids, context=None):
748 user_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
749 users = self.pool.get('res.users').read(cr, uid, user_ids, fields=['id', 'name', 'avatar'], context=context)
752 def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
753 users = self.message_get_subscribers(cr, uid, ids, context=context)
754 sub_user_id = uid if user_id is None else user_id
755 if sub_user_id in [user['id'] for user in users]:
759 def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
760 subscription_obj = self.pool.get('mail.subscription')
761 to_subscribe_uids = [uid] if user_ids is None else user_ids
764 for user_id in to_subscribe_uids:
765 if self.message_is_subscriber(cr, uid, [id], user_id=user_id, context=context): continue
766 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
769 def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
770 if not user_ids and not uid in self.message_get_subscribers_ids(cr, uid, ids, context=context):
772 subscription_obj = self.pool.get('mail.subscription')
773 to_unsubscribe_uids = [uid] if user_ids is None else user_ids
774 to_delete_sub_ids = subscription_obj.search(cr, uid,
775 ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
776 subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
779 #------------------------------------------------------
781 #------------------------------------------------------
783 def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
786 notif_obj = self.pool.get('mail.notification')
787 msg_obj = self.pool.get('mail.message')
789 notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
791 notif_msg_ids = msg_ids
792 to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
793 return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
795 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: