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 def _search_message_ids(self, cr, uid, obj, name, args, context=None):
72 msg_obj = self.pool.get('mail.message')
73 msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
74 return [('id', 'in', msg_ids)]
76 # OpenChatter: message_ids is a dummy field that should not be used
78 'message_ids': fields.function(_get_message_ids, method=True, fnct_search=_search_message_ids,
79 type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'),
82 #------------------------------------------------------
83 # Automatic subscription when creating/reading
84 #------------------------------------------------------
86 def create(self, cr, uid, vals, context=None):
87 """Automatically subscribe the creator"""
88 thread_id = super(mail_thread, self).create(cr, uid, vals, context=context);
89 self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
92 def write(self, cr, uid, ids, vals, context=None):
93 """Automatically subscribe the writer"""
94 if isinstance(ids, (int, long)):
96 write_res = super(mail_thread, self).write(cr, uid, ids, vals, context=context);
98 self.message_subscribe(cr, uid, ids, [uid], context=context)
101 def unlink(self, cr, uid, ids, context=None):
102 """Override unlink, to automatically delete
105 that are linked with res_model and res_id, not through
106 a foreign key with a 'cascade' ondelete attribute.
107 Notifications will be deleted with messages
111 subscr_obj = self.pool.get('mail.subscription')
112 msg_obj = self.pool.get('mail.message')
113 # delete subscriptions
114 subscr_to_del_ids = subscr_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
115 subscr_obj.unlink(cr, uid, subscr_to_del_ids, context=context)
116 # delete notifications
117 msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
118 msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
120 return super(mail_thread, self).unlink(cr, uid, ids, context=context)
122 #------------------------------------------------------
123 # Generic message api
124 #------------------------------------------------------
126 def message_create(self, cr, uid, thread_id, vals, context=None):
127 """OpenSocial: wrapper of mail.message create method
128 - creates the mail.message
129 - automatically subscribe the message writer
130 - push the message to subscribed users
134 message_obj = self.pool.get('mail.message')
135 subscription_obj = self.pool.get('mail.subscription')
136 notification_obj = self.pool.get('mail.notification')
137 res_users_obj = self.pool.get('res.users')
138 body = vals.get('body_html', '') if vals.get('subtype', 'plain') == 'html' else vals.get('body_text', '')
140 # automatically subscribe the writer of the message
142 self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
144 # get users that will get a notification pushed
145 user_to_push_ids = self.message_create_get_notification_user_ids(cr, uid, [thread_id], vals, context=context)
146 user_to_push_from_parse_ids = self.message_parse_users(cr, uid, [thread_id], body, context=context)
148 # set email_from and email_to for comments and notifications
149 if vals.get('type', False) and vals['type'] == 'comment' or vals['type'] == 'notification':
150 current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
151 if not vals.get('email_from', False):
152 vals['email_from'] = current_user.user_email
153 if not vals.get('email_to', False):
155 for user in res_users_obj.browse(cr, uid, user_to_push_ids, context=context):
156 if not user.notification_email_pref == 'all' and \
157 not (user.notification_email_pref == 'comments' and vals['type'] == 'comment') and \
158 not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
160 if not user.user_email:
162 email_to = '%s, %s' % (email_to, user.user_email)
163 email_to = email_to.lstrip(', ')
165 vals['email_to'] = email_to
166 vals['state'] = 'outgoing'
169 msg_id = message_obj.create(cr, uid, vals, context=context)
171 # special: if install mode, do not push demo data
172 if context.get('install_mode', False):
176 for id in user_to_push_ids:
177 notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
181 def message_create_get_notification_user_ids(self, cr, uid, thread_ids, new_msg_vals, context=None):
186 body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
187 for thread_id in thread_ids:
189 notif_user_ids += [user['id'] for user in self.message_get_subscribers(cr, uid, [thread_id], context=context)]
190 # add users requested via parsing message (@login)
191 notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
192 # add users requested to perform an action (need_action mechanism)
193 if hasattr(self, 'get_needaction_user_ids'):
194 notif_user_ids += self.get_needaction_user_ids(cr, uid, [thread_id], context=context)[thread_id]
195 # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
196 if new_msg_vals.get('parent_id'):
197 notif_obj = self.pool.get('mail.notification')
198 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
199 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
200 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
202 # remove duplicate entries
203 notif_user_ids = list(set(notif_user_ids))
204 return notif_user_ids
206 def message_parse_users(self, cr, uid, ids, string, context=None):
207 """Parse message content
208 - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
209 this supports login that are emails (such as @admin@lapin.net)
211 regex = re.compile('(^|\s)@((\w|@|\.)*)')
212 login_lst = [item[1] for item in regex.findall(string)]
213 if not login_lst: return []
214 user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
217 def message_capable_models(self, cr, uid, context=None):
219 for model_name in self.pool.obj_list():
220 model = self.pool.get(model_name)
221 if 'mail.thread' in getattr(model, '_inherit', []):
222 ret_dict[model_name] = model._description
225 def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
226 parent_id=False, type='email', subtype='plain', state='received',
227 email_to=False, email_from=False, email_cc=None, email_bcc=None,
228 reply_to=None, email_date=None, message_id=False, references=None,
229 attachments=None, headers=None, original=None, context=None):
230 """Creates a new mail.message attached to the current mail.thread,
231 containing all the details passed as parameters. All attachments
232 will be attached to the thread record as well as to the actual
234 If ``email_from`` is not set or ``type`` not set as 'email',
235 a note message is created, without the usual envelope
236 attributes (sender, recipients, etc.).
237 The creation of the message is done by calling ``message_create``
238 method, that will manage automatic pushing of notifications.
240 :param threads: list of thread ids, or list of browse_records representing
241 threads to which a new message should be attached
242 :param subject: subject of the message, or description of the event if this
243 is an *event log* entry.
244 :param body_text: plaintext contents of the mail or log message
245 :param body_html: html contents of the mail or log message
246 :param parent_id: id of the parent message (threaded messaging model)
247 :param type: optional type of message: 'email', 'comment', 'notification'
248 :param subtype: optional subtype of message: 'plain' or 'html', corresponding to the main
249 body contents (body_text or body_html).
250 :param state: optional state of message; 'received' by default
251 :param email_to: Email-To / Recipient address
252 :param email_from: Email From / Sender address if any
253 :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
254 :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
255 :param reply_to: reply_to header
256 :param email_date: email date string if different from now, in server timezone
257 :param message_id: optional email identifier
258 :param references: optional email references
259 :param headers: mail headers to store
260 :param dict attachments: map of attachment filenames to binary contents, if any.
261 :param str original: optional full source of the RFC2822 email, for reference
262 :param dict context: if a ``thread_model`` value is present
263 in the context, its value will be used
264 to determine the model of the thread to
265 update (instead of the current model).
269 if attachments is None:
273 edate = parsedate(email_date)
274 if edate is not None:
275 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
277 if all(isinstance(thread_id, (int, long)) for thread_id in threads):
278 model = context.get('thread_model') or self._name
279 model_pool = self.pool.get(model)
280 threads = model_pool.browse(cr, uid, threads, context=context)
282 ir_attachment = self.pool.get('ir.attachment')
283 mail_message = self.pool.get('mail.message')
286 for thread in threads:
288 for attachment in attachments:
289 fname, fcontent = attachment
290 if isinstance(fcontent, unicode):
291 fcontent = fcontent.encode('utf-8')
294 'datas': base64.b64encode(str(fcontent)),
295 'datas_fname': fname,
296 'description': _('Mail attachment'),
297 'res_model': thread._name,
300 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
302 partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
303 if not partner_id and thread._name == 'res.partner':
304 partner_id = thread.id
307 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
308 'body_html': body_html or '',
309 'parent_id': parent_id,
310 'date': email_date or fields.datetime.now(),
314 'message_id': message_id,
315 'attachment_ids': [(6, 0, to_attach)],
317 'model' : thread._name,
319 'partner_id': partner_id,
322 if email_from or type == 'email':
323 for param in (email_to, email_cc, email_bcc):
324 if isinstance(param, list):
325 param = ", ".join(param)
327 'subject': subject or _('History'),
328 'email_to': email_to,
329 'email_from': email_from or \
330 (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
331 'email_cc': email_cc,
332 'email_bcc': email_bcc,
333 'references': references,
335 'reply_to': reply_to,
336 'original': original, })
338 new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
341 def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
342 """Creates a new mail.message attached to the given threads (``ids``),
343 with the contents of ``msg_dict``, by calling ``message_append``
344 with the mail details. All attachments in msg_dict will be
345 attached to the object record as well as to the actual
348 :param dict msg_dict: a map containing the email details and
349 attachments. See ``message_process()`` and
350 ``mail.message.parse()`` for details on
352 :param dict context: if a ``thread_model`` value is present
353 in the context, its value will be used
354 to determine the model of the thread to
355 update (instead of the current model).
357 return self.message_append(cr, uid, ids,
358 subject = msg_dict.get('subject'),
359 body_text = msg_dict.get('body_text'),
360 body_html= msg_dict.get('body_html'),
361 parent_id = msg_dict.get('parent_id', False),
362 type = msg_dict.get('type', 'email'),
363 subtype = msg_dict.get('subtype', 'plain'),
364 state = msg_dict.get('state', 'received'),
365 email_from = msg_dict.get('from', msg_dict.get('email_from')),
366 email_to = msg_dict.get('to', msg_dict.get('email_to')),
367 email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
368 email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
369 reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
370 email_date = msg_dict.get('date'),
371 message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
372 references = msg_dict.get('references')\
373 or msg_dict.get('in-reply-to'),
374 attachments = msg_dict.get('attachments'),
375 headers = msg_dict.get('headers'),
376 original = msg_dict.get('original'),
380 def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
381 """ Given message child_ids
382 Find their ancestors until root ids"""
385 msg_obj = self.pool.get('mail.message')
386 tmp_msgs = msg_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
387 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]
388 child_ids += parent_ids
389 cur_iter = 0; max_iter = 100; # avoid infinite loop
390 while (parent_ids and (cur_iter < max_iter)):
392 tmp_msgs = msg_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
393 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]
394 child_ids += parent_ids
395 if (cur_iter > max_iter):
396 _logger.warning("Possible infinite loop in _message_add_ancestor_ids. Note that this algorithm is intended to check for cycle in message graph.")
399 def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
400 """ OpenChatter feature: return thread messages ids. It searches in
401 mail.messages where res_id = ids, (res_)model = current model.
402 :param domain: domain to add to the search; especially child_of
403 is interesting when dealing with threaded display
404 :param ascent: performs an ascended search; will add to fetched msgs
405 all their parents until root_ids
406 :param root_ids: for ascent search
407 :param root_ids: root_ids when performing an ascended search
411 msg_obj = self.pool.get('mail.message')
412 msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', ids), ('model', '=', self._name)] + domain,
413 limit=limit, offset=offset, context=context)
414 if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
417 def message_load(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
418 """ OpenChatter feature: return thread messages
420 msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
421 msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
423 """ Retrieve all attachments names """
427 for attach_id in msg["attachment_ids"]:
428 map_id_to_name[attach_id] = '' # use empty string as a placeholder
430 ids = map_id_to_name.keys()
431 names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
433 # convert the list of tuples into a dictionnary
435 map_id_to_name[name[0]] = name[1]
437 # give corresponding ids and names to each message
439 msg["attachments"] = []
441 for attach_id in msg["attachment_ids"]:
442 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
444 """ Sort and return messages """
445 msgs = sorted(msgs, key=lambda d: (-d['id']))
448 def get_pushed_messages(self, cr, uid, ids, limit=100, offset=0, msg_search_domain=[], ascent=False, root_ids=[], context=None):
449 """ OpenChatter: wall: get messages to display (=pushed notifications)
450 :param domain: domain to add to the search; especially child_of
451 is interesting when dealing with threaded display
452 :param ascent: performs an ascended search; will add to fetched msgs
453 all their parents until root_ids
454 :param root_ids: for ascent search
455 :return list of mail.messages sorted by date
457 if context is None: context = {}
458 notification_obj = self.pool.get('mail.notification')
459 msg_obj = self.pool.get('mail.message')
460 # update message search
461 for arg in msg_search_domain:
462 if isinstance(arg, (tuple, list)):
463 arg[0] = 'message_id.' + arg[0]
464 # compose final domain
465 domain = [('user_id', '=', uid)] + msg_search_domain
467 notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
468 notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
469 msg_ids = [notification.message_id.id for notification in notifications]
471 msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
472 if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
473 msgs = msg_obj.read(cr, uid, msg_ids, context=context)
476 #------------------------------------------------------
478 #------------------------------------------------------
479 # message_process will call either message_new or message_update.
481 def message_process(self, cr, uid, model, message, custom_values=None,
482 save_original=False, strip_attachments=False,
484 """Process an incoming RFC2822 email message related to the
485 given thread model, relying on ``mail.message.parse()``
486 for the parsing operation, and then calling ``message_new``
487 (if the thread record did not exist) or ``message_update``
488 (if it did), then calling ``message_forward`` to automatically
489 notify other people that should receive this message.
491 :param string model: the thread model for which a new message
493 :param message: source of the RFC2822 mail
494 :type message: string or xmlrpclib.Binary
495 :type dict custom_values: optional dictionary of field values
496 to pass to ``message_new`` if a new
497 record needs to be created. Ignored
498 if the thread record already exists.
499 :param bool save_original: whether to keep a copy of the original
500 email source attached to the message after it is imported.
501 :param bool strip_attachments: whether to strip all attachments
502 before processing the message, in order to save some space.
504 # extract message bytes - we are forced to pass the message as binary because
505 # we don't know its encoding until we parse its headers and hence can't
506 # convert it to utf-8 for transport between the mailgate script and here.
507 if isinstance(message, xmlrpclib.Binary):
508 message = str(message.data)
510 model_pool = self.pool.get(model)
511 if self._name != model:
512 if context is None: context = {}
513 context.update({'thread_model': model})
515 mail_message = self.pool.get('mail.message')
519 # Warning: message_from_string doesn't always work correctly on unicode,
520 # we must use utf-8 strings here :-(
521 if isinstance(message, unicode):
522 message = message.encode('utf-8')
523 msg_txt = email.message_from_string(message)
524 msg = mail_message.parse_message(msg_txt, save_original=save_original)
526 if strip_attachments and 'attachments' in msg:
527 del msg['attachments']
529 # Create New Record into particular model
530 def create_record(msg):
531 if hasattr(model_pool, 'message_new'):
532 return model_pool.message_new(cr, uid, msg,
536 if msg.get('references') or msg.get('in-reply-to'):
537 references = msg.get('references') or msg.get('in-reply-to')
538 if '\r\n' in references:
539 references = references.split('\r\n')
541 references = references.split(' ')
542 for ref in references:
544 res_id = tools.reference_re.search(ref)
546 res_id = res_id.group(1)
548 res_id = tools.res_re.search(msg['subject'])
550 res_id = res_id.group(1)
553 if model_pool.exists(cr, uid, res_id):
554 if hasattr(model_pool, 'message_update'):
555 model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
557 # referenced thread was not found, we'll have to create a new one
560 res_id = create_record(msg)
561 #To forward the email to other followers
562 self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
565 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
566 """Called by ``message_process`` when a new message is received
567 for a given thread model, if the message did not belong to
569 The default behavior is to create a new record of the corresponding
570 model (based on some very basic info extracted from the message),
571 then attach the message to the newly created record
572 (by calling ``message_append_dict``).
573 Additional behavior may be implemented by overriding this method.
575 :param dict msg_dict: a map containing the email details and
576 attachments. See ``message_process`` and
577 ``mail.message.parse`` for details.
578 :param dict custom_values: optional dictionary of additional
579 field values to pass to create()
580 when creating the new thread record.
581 Be careful, these values may override
582 any other values coming from the message.
583 :param dict context: if a ``thread_model`` value is present
584 in the context, its value will be used
585 to determine the model of the record
586 to create (instead of the current model).
588 :return: the id of the newly created thread object
592 model = context.get('thread_model') or self._name
593 model_pool = self.pool.get(model)
594 fields = model_pool.fields_get(cr, uid, context=context)
595 data = model_pool.default_get(cr, uid, fields, context=context)
596 if 'name' in fields and not data.get('name'):
597 data['name'] = msg_dict.get('from','')
598 if custom_values and isinstance(custom_values, dict):
599 data.update(custom_values)
600 res_id = model_pool.create(cr, uid, data, context=context)
601 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
604 def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
605 """Called by ``message_process`` when a new message is received
606 for an existing thread. The default behavior is to create a
607 new mail.message in the given thread (by calling
608 ``message_append_dict``)
609 Additional behavior may be implemented by overriding this
612 :param dict msg_dict: a map containing the email details and
613 attachments. See ``message_process`` and
614 ``mail.message.parse()`` for details.
615 :param dict context: if a ``thread_model`` value is present
616 in the context, its value will be used
617 to determine the model of the thread to
618 update (instead of the current model).
620 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
622 def message_thread_followers(self, cr, uid, ids, context=None):
623 """Returns a list of email addresses of the people following
624 this thread, including the sender of each mail, and the
625 people who were in CC of the messages, if any.
628 if isinstance(ids, (str, int, long)):
630 for thread in self.browse(cr, uid, ids, context=context):
632 for message in thread.message_ids:
633 l.add((message.user_id and message.user_id.user_email) or '')
634 l.add(message.email_from or '')
635 l.add(message.email_cc or '')
636 res[thread.id] = filter(None, l)
639 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
640 """Sends an email to all people following the given threads.
641 The emails are forwarded immediately, not queued for sending,
644 :param str model: thread model
645 :param list thread_ids: ids of the thread records
646 :param msg: email.message.Message object to forward
647 :param email_error: optional email address to notify in case
648 of any delivery error during the forward.
651 model_pool = self.pool.get(model)
652 smtp_server_obj = self.pool.get('ir.mail_server')
653 mail_message = self.pool.get('mail.message')
654 for res in model_pool.browse(cr, uid, thread_ids, context=context):
655 if hasattr(model_pool, 'message_thread_followers'):
656 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
658 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
659 message_followers_emails = to_email(','.join(filter(None, followers)))
660 message_recipients = to_email(','.join(filter(None,
661 [decode(msg['from']),
663 decode(msg['cc'])])))
664 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
666 # TODO: we need an interface for this for all types of objects, not just leads
667 if hasattr(res, 'section_id'):
669 msg['reply-to'] = res.section_id.reply_to
671 smtp_from, = to_email(msg['from'])
672 msg['from'] = smtp_from
673 msg['to'] = ", ".join(forward_to)
674 msg['message-id'] = tools.generate_tracking_message_id(res.id)
675 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
676 subj = msg['subject']
677 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
678 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
679 msg['to'] = email_error
680 smtp_server_obj.send_email(cr, uid, msg)
683 def message_partner_by_email(self, cr, uid, email, context=None):
684 """Attempts to return the id of a partner address matching
685 the given ``email``, and the corresponding partner id.
686 Can be used by classes using the ``mail.thread`` mixin
687 to lookup the partner and use it in their implementation
688 of ``message_new`` to link the new record with a
689 corresponding partner.
690 The keys used in the returned dict are meant to map
691 to usual names for relationships towards a partner
692 and one of its addresses.
694 :param email: email address for which a partner
695 should be searched for.
697 :return: a map of the following form::
699 { 'partner_address_id': id or False,
700 'partner_id': pid or False }
702 partner_pool = self.pool.get('res.partner')
703 res = {'partner_id': False}
705 email = to_email(email)[0]
706 contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
708 contact = partner_pool.browse(cr, uid, contact_ids[0])
709 res['partner_id'] = contact.id
712 # for backwards-compatibility with old scripts
713 process_email = message_process
715 #------------------------------------------------------
717 #------------------------------------------------------
719 def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
722 notification_obj = self.pool.get('mail.notification')
724 msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, subtype=subtype, context=context)
725 # escape if in install mode or note writing was not successfull
726 if 'install_mode' in context:
728 if not isinstance(msg_ids, (list)):
730 # get already existing notigications
731 notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
732 already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
733 # get base.group_user group
734 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
735 group_id = res and res[1] or False
736 if not group_id: return True
737 group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
738 for user in group.users:
739 if user.id in already_pushed_user_ids: continue
740 for msg_id in msg_ids:
741 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
744 def log(self, cr, uid, id, message, secondary=False, context=None):
745 _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
746 self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
748 def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
750 if type == 'notification':
751 subject = _('System notification')
752 elif type == 'comment' and not parent_id:
753 subject = _('Comment')
754 elif type == 'comment' and parent_id:
756 if subtype == 'html':
762 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)
764 #------------------------------------------------------
765 # Subscription mechanism
766 #------------------------------------------------------
768 def message_get_subscribers_ids(self, cr, uid, ids, context=None):
769 subscr_obj = self.pool.get('mail.subscription')
770 subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
771 subs = subscr_obj.read(cr, uid, subscr_ids, context=context)
772 return [sub['user_id'][0] for sub in subs]
774 def message_get_subscribers(self, cr, uid, ids, context=None):
775 user_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
776 users = self.pool.get('res.users').read(cr, uid, user_ids, fields=['id', 'name', 'image_small'], context=context)
779 def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
780 users = self.message_get_subscribers(cr, uid, ids, context=context)
781 sub_user_id = uid if user_id is None else user_id
782 if sub_user_id in [user['id'] for user in users]:
786 def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
787 subscription_obj = self.pool.get('mail.subscription')
788 to_subscribe_uids = [uid] if user_ids is None else user_ids
791 for user_id in to_subscribe_uids:
792 if self.message_is_subscriber(cr, uid, [id], user_id=user_id, context=context): continue
793 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
796 def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
797 if not user_ids and not uid in self.message_get_subscribers_ids(cr, uid, ids, context=context):
799 subscription_obj = self.pool.get('mail.subscription')
800 to_unsubscribe_uids = [uid] if user_ids is None else user_ids
801 to_delete_sub_ids = subscription_obj.search(cr, uid,
802 ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
803 subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
806 #------------------------------------------------------
808 #------------------------------------------------------
810 def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
813 notif_obj = self.pool.get('mail.notification')
814 msg_obj = self.pool.get('mail.message')
816 notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
818 notif_msg_ids = msg_ids
819 to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
820 return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
822 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: