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 ##############################################################################
28 from email.utils import parsedate
29 from email.message import Message
31 from osv import osv, fields
32 from mail_message import decode, to_email
34 from tools.translate import _
35 from tools.safe_eval import safe_eval as eval
37 _logger = logging.getLogger(__name__)
39 def decode_header(message, header, separator=' '):
40 return separator.join(map(decode,message.get_all(header, [])))
42 class many2many_reference(fields.many2many):
43 """ many2many_reference is an override of fields.many2many. It manages
44 many2many-like table where one id is given by two fields, res_model
48 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
50 - mail_followers.res_model = 'crm.lead'
52 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
53 FROM %(rel)s, %(from_c)s \
54 WHERE %(rel)s.%(id1)s IN %%s \
55 AND %(rel)s.%(id2)s = %(tbl)s.id \
56 AND %(rel)s.res_model = %%s \
62 where_params = [model._name] + where_params
63 return query, where_params
65 def set(self, cr, model, id, name, values, user=None, context=None):
66 """ Override to add the res_model field in queries. """
68 rel, id1, id2 = self._sql_names(model)
69 obj = model.pool.get(self._obj)
71 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
74 idnew = obj.create(cr, user, act[2], context=context)
75 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, idnew, model._name))
77 cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND res_model=%s', (id, act[1], model._name))
79 # following queries are in the same transaction - so should be relatively safe
80 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND res_model=%s', (id, act[1], model._name))
82 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, act[1], model._name))
84 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
86 d1 = ' and ' + ' and '.join(d1)
89 cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND res_model=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2)
90 for act_nbr in act[2]:
91 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, act_nbr, model._name))
93 return super(many2many_reference, self).set(cr, model, id, name, values, user, context)
95 class mail_thread(osv.Model):
96 '''Mixin model, meant to be inherited by any model that needs to
97 act as a discussion topic on which messages can be attached.
98 Public methods are prefixed with ``message_`` in order to avoid
99 name collisions with methods of the models that will inherit
102 ``mail.thread`` is designed to work without adding any field
103 to the extended models. All functionalities and expected behavior
104 are managed by mail.thread, using model name and record ids.
105 A widget has been designed for the 6.1 and following version of OpenERP
106 web-client. However, due to technical limitations, ``mail.thread``
107 adds a simulated one2many field, to display the web widget by
108 overriding the default field displayed. Using this field
109 is not recommanded has it will disappeear in future version
110 of OpenERP, leading to a pure mixin class.
112 Inheriting classes are not required to implement any method, as the
113 default implementation will work for any model. However it is common
114 to override at least the ``message_new`` and ``message_update``
115 methods (calling ``super``) to add model-specific behavior at
116 creation and update of a thread.
118 #TODO: UPDATE WITH SUBTYPE / NEW FOLLOW MECHANISM
120 _name = 'mail.thread'
121 _description = 'Email Thread'
123 def _get_message_data(self, cr, uid, ids, field_names, args, context=None):
124 res = dict.fromkeys(ids)
126 res[id] = {'message_ids': self.message_search(cr, uid, [id], context=context)}
127 for thread in self.browse(cr, uid, ids, context=context):
128 message_follower_ids = [follower.id for follower in thread.message_follower_ids]
129 res[thread.id].update({
130 'message_is_follower': uid in message_follower_ids,
131 'message_summary': "<span><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" %
132 (len(res[thread.id]['message_ids']), len(thread.message_follower_ids))
136 def _search_message_ids(self, cr, uid, obj, name, args, context=None):
137 msg_obj = self.pool.get('mail.message')
138 msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
139 return [('id', 'in', msg_ids)]
142 'message_ids': fields.function(_get_message_data,
143 fnct_search=_search_message_ids,
144 type='one2many', obj='mail.message', _fields_id = 'res_id',
145 string='Messages', multi="_get_message_data",
146 help="Field holding discussion about the current document."),
147 'message_follower_ids': many2many_reference('res.users',
148 rel='mail_followers', id1='res_id', id2='user_id', string="Followers",
149 help="Followers of the document. The followers have full access to " \
150 "the document details, as well as the conversation."),
151 'message_is_follower': fields.function(_get_message_data, method=True,
152 type='boolean', string='I am Follower', multi='_get_message_data',
153 help='True if the current user is following the current document.'),
154 'message_state': fields.boolean('Read',
155 help="When checked, new messages require your attention."),
156 'message_summary': fields.function(_get_message_data, method=True,
157 type='text', string='Summary', multi='_get_message_data',
158 help="Holds the Chatter summary (number of messages, ...). "\
159 "This summary is directly in html format in order to "\
160 "be inserted in kanban views."),
164 'message_state': True,
167 #------------------------------------------------------
168 # Automatic subscription when creating/reading
169 #------------------------------------------------------
171 def create(self, cr, uid, vals, context=None):
172 """ Override of create to subscribe :
174 - followers given by the monitored fields
176 thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
177 followers_command = self.message_get_automatic_followers(cr, uid, thread_id, vals, fetch_missing=False, context=context)
178 if followers_command:
179 self.write(cr, uid, [thread_id], {'message_follower_ids': followers_command}, context=context)
182 def write(self, cr, uid, ids, vals, context=None):
183 """ Override of write to subscribe :
185 - followers given by the monitored fields
187 if isinstance(ids, (int, long)):
190 # copy original vals because we are going to modify it
191 specific_vals = dict(vals)
192 # we modify followers: do not subscribe the uid
193 if specific_vals.get('message_follower_ids'):
194 followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, add_uid=False, context=context)
195 specific_vals['message_follower_ids'] += followers_command
197 followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, context=context)
198 specific_vals['message_follower_ids'] = followers_command
199 write_res = super(mail_thread, self).write(cr, uid, ids, specific_vals, context=context)
202 def unlink(self, cr, uid, ids, context=None):
203 """Override unlink, to automatically delete messages
204 that are linked with res_model and res_id, not through
205 a foreign key with a 'cascade' ondelete attribute.
206 Notifications will be deleted with messages
208 msg_obj = self.pool.get('mail.message')
209 # delete messages and notifications
210 msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
211 msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
212 return super(mail_thread, self).unlink(cr, uid, ids, context=context)
214 def message_get_automatic_followers(self, cr, uid, id, record_vals, add_uid=True, fetch_missing=False, context=None):
215 """ Return the command for the many2many follower_ids field to manage
216 subscribers. Behavior :
217 - get the monitored fields (ex: ['user_id', 'responsible_id']); those
218 fields should be relationships to res.users (#TODO: res.partner)
219 - if this field is in the record_vals: it means it has been modified
220 thus add its value to the followers
221 - if this fields is not in record_vals, but fetch_missing paramter
222 is set to True: fetch the value in the record (use: at creation
223 for default values, not present in record_vals)
224 - if add_uid: add the current user (for example: writer is subscriber)
225 - generate the command and return it
226 This method has to be used on 1 id, because otherwise it would imply
227 to track which user.id is used for which record.id.
229 :param record_vals: values given to the create method of the new
230 record, or values updated in a write.
231 :param monitored_fields: a list of fields that are monitored. Those
232 fields must be many2one fields to the res.users model.
233 :param fetch_missing: is set to True, the method will read the
234 record to find values that are not present in record_vals.
236 #TODO : UPDATE WHEN MERGING TO PARTNERS
238 # get monitored fields
239 monitored_fields = self.message_get_monitored_follower_fields(cr, uid, [id], context=context)
240 modified_fields = [field for field in monitored_fields if field in record_vals.iterkeys()]
241 other_fields = [field for field in monitored_fields if field not in record_vals.iterkeys()] if fetch_missing else []
242 # for each monitored field: if in record_vals, it has been modified/added
244 for field in modified_fields:
246 if record_vals.get(fields):
247 follower_ids.append(record_vals.get(field))
248 # for other fields: read in record if fetch_missing (otherwise list is void)
249 for field in other_fields:
250 record = self.browse(cr, uid, id, context=context)
251 value = getattr(record, field)
253 follower_ids.append(value)
254 # add uid if asked and not already present
255 if add_uid and uid not in follower_ids:
256 follower_ids.append(uid)
257 return self.message_subscribe_get_command(cr, uid, follower_ids, context=context)
259 #------------------------------------------------------
260 # mail.message wrappers and tools
261 #------------------------------------------------------
263 def message_create(self, cr, uid, thread_id, vals, context=None):
264 """ OpenChatter: wrapper of mail.message create method
265 - creates the mail.message
266 - automatically subscribe the message writer
267 - push the message to followers
273 msg_id = self.pool.get('mail.message').create(cr, uid, vals, context=context)
275 # automatically subscribe the writer of the message
276 if vals.get('user_id'):
277 record = self.browse(cr, uid, thread_id, context=context)
278 follower_ids = [follower.id for follower in record.message_follower_ids]
279 if vals.get('user_id') not in follower_ids:
280 self.message_subscribe(cr, uid, [thread_id], [vals.get('user_id')], context=context)
282 # Set as unread if writer is not the document responsible
283 self.message_create_set_unread(cr, uid, [thread_id], context=context)
285 # special: if install mode, do not push demo data
286 if context.get('install_mode', False):
289 # get users that will get a notification pushed
290 notification_obj = self.pool.get('mail.notification')
291 user_to_push_ids = self.message_get_user_ids_to_notify(cr, uid, [thread_id], vals, context=context)
292 for id in user_to_push_ids:
293 notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
295 # create the email to send
296 self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
300 def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
302 body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
305 subscr_obj = self.pool.get('mail.followers')
306 subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', thread_ids)], context=context)
307 notif_user_ids = [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)]
309 # add users requested to perform an action (need_action mechanism)
310 if hasattr(self, 'get_needaction_user_ids') and self._columns.get('user_id'):
311 user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context)
312 for id, user_ids in user_ids_dict.iteritems():
313 notif_user_ids += user_ids
315 # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
316 if new_msg_vals.get('parent_id'):
317 notif_obj = self.pool.get('mail.notification')
318 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
319 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
320 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
322 # remove duplicate entries
323 notif_user_ids = list(set(notif_user_ids))
324 return notif_user_ids
326 #------------------------------------------------------
327 # Generic message api
328 #------------------------------------------------------
330 def message_capable_models(self, cr, uid, context=None):
332 for model_name in self.pool.obj_list():
333 model = self.pool.get(model_name)
334 if 'mail.thread' in getattr(model, '_inherit', []):
335 ret_dict[model_name] = model._description
338 def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
339 type='email', email_date=None, parent_id=False,
340 content_subtype='plain', state=None,
341 partner_ids=None, email_from=False, email_to=False,
342 email_cc=None, email_bcc=None, reply_to=None,
343 headers=None, message_id=False, references=None,
344 attachments=None, original=None, context=None):
345 """ Creates a new mail.message through message_create. The new message
346 is attached to the current mail.thread, containing all the details
347 passed as parameters. All attachments will be attached to the
348 thread record as well as to the actual message.
350 This method calls message_create that will handle management of
351 subscription and notifications, and effectively create the message.
353 If ``email_from`` is not set or ``type`` not set as 'email',
354 a note message is created (comment or system notification),
355 without the usual envelope attributes (sender, recipients, etc.).
357 :param threads: list of thread ids, or list of browse_records
358 representing threads to which a new message should be attached
359 :param subject: subject of the message, or description of the event;
360 this is totally optional as subjects are not important except
361 for specific messages (blog post, job offers) or for emails
362 :param body_text: plaintext contents of the mail or log message
363 :param body_html: html contents of the mail or log message
364 :param type: type of message: 'email', 'comment', 'notification';
366 :param email_date: email date string if different from now, in
368 :param parent_id: id of the parent message (threaded messaging model)
369 :param content_subtype: optional content_subtype of message: 'plain'
370 or 'html', corresponding to the main body contents (body_text or
372 :param state: state of message
373 :param partner_ids: destination partners of the message, in addition
374 to the now fully optional email_to; this method is supposed to
375 received a list of ids is not None. The specific many2many
376 instruction will be generated by this method.
377 :param email_from: Email From / Sender address if any
378 :param email_to: Email-To / Recipient address
379 :param email_cc: Comma-Separated list of Carbon Copy Emails To
381 :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To
383 :param reply_to: reply_to header
384 :param headers: mail headers to store
385 :param message_id: optional email identifier
386 :param references: optional email references
387 :param dict attachments: map of attachment filenames to binary
389 :param str original: optional full source of the RFC2822 email, for
391 :param dict context: if a ``thread_model`` value is present in the
392 context, its value will be used to determine the model of the
393 thread to update (instead of the current model).
397 if attachments is None:
401 edate = parsedate(email_date)
402 if edate is not None:
403 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
405 if all(isinstance(thread_id, (int, long)) for thread_id in threads):
406 model = context.get('thread_model') or self._name
407 model_pool = self.pool.get(model)
408 threads = model_pool.browse(cr, uid, threads, context=context)
410 ir_attachment = self.pool.get('ir.attachment')
413 for thread in threads:
415 for attachment in attachments:
416 fname, fcontent = attachment
417 if isinstance(fcontent, unicode):
418 fcontent = fcontent.encode('utf-8')
421 'datas': base64.b64encode(str(fcontent)),
422 'datas_fname': fname,
423 'description': _('Mail attachment'),
424 'res_model': thread._name,
427 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
428 # find related partner: partner_id column in thread object, or self is res.partner model
429 partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
430 if not partner_id and thread._name == 'res.partner':
431 partner_id = thread.id
432 # destination partners
433 if partner_ids is None:
435 mail_partner_ids = [(6, 0, partner_ids)]
439 'body_text': body_text or thread._model._columns.get('description') and thread.description or '',
440 'body_html': body_html or '',
441 'parent_id': parent_id,
442 'date': email_date or fields.datetime.now(),
444 'content_subtype': content_subtype,
446 'message_id': message_id,
447 'partner_ids': mail_partner_ids,
448 'attachment_ids': [(6, 0, to_attach)],
450 'model' : thread._name,
452 'partner_id': partner_id,
455 if email_from or type == 'email':
456 for param in (email_to, email_cc, email_bcc):
457 if isinstance(param, list):
458 param = ", ".join(param)
460 'email_to': email_to,
461 'email_from': email_from or \
462 thread._model._columns.get('user_id') and thread.user_id and thread.user_id.user_email,
463 'email_cc': email_cc,
464 'email_bcc': email_bcc,
465 'references': references,
467 'reply_to': reply_to,
468 'original': original, })
470 new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
473 def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
474 """Creates a new mail.message attached to the given threads (``ids``),
475 with the contents of ``msg_dict``, by calling ``message_append``
476 with the mail details. All attachments in msg_dict will be
477 attached to the object record as well as to the actual
480 :param dict msg_dict: a map containing the email details and
481 attachments. See ``message_process()`` and
482 ``mail.message.parse()`` for details on
484 :param dict context: if a ``thread_model`` value is present
485 in the context, its value will be used
486 to determine the model of the thread to
487 update (instead of the current model).
489 return self.message_append(cr, uid, ids,
490 subject = msg_dict.get('subject'),
491 body_text = msg_dict.get('body_text'),
492 body_html= msg_dict.get('body_html'),
493 parent_id = msg_dict.get('parent_id', False),
494 type = msg_dict.get('type', 'email'),
495 content_subtype = msg_dict.get('content_subtype'),
496 state = msg_dict.get('state'),
497 partner_ids = msg_dict.get('partner_ids'),
498 email_from = msg_dict.get('from', msg_dict.get('email_from')),
499 email_to = msg_dict.get('to', msg_dict.get('email_to')),
500 email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
501 email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
502 reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
503 email_date = msg_dict.get('date'),
504 message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
505 references = msg_dict.get('references')\
506 or msg_dict.get('in-reply-to'),
507 attachments = msg_dict.get('attachments'),
508 headers = msg_dict.get('headers'),
509 original = msg_dict.get('original'),
512 #------------------------------------------------------
514 #------------------------------------------------------
516 def _message_search_ancestor_ids(self, cr, uid, ids, child_ids, ancestor_ids, context=None):
517 """ Given message child_ids ids, find their ancestors until ancestor_ids
518 using their parent_id relationship.
520 :param child_ids: the first nodes of the search
521 :param ancestor_ids: list of ancestors. When the search reach an
524 def _get_parent_ids(message_list, ancestor_ids, child_ids):
525 """ Tool function: return the list of parent_ids of messages
526 contained in message_list. Parents that are in ancestor_ids
527 or in child_ids are not returned. """
528 return [message['parent_id'][0] for message in message_list
529 if message['parent_id']
530 and message['parent_id'][0] not in ancestor_ids
531 and message['parent_id'][0] not in child_ids
534 message_obj = self.pool.get('mail.message')
535 messages_temp = message_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
536 parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
537 child_ids += parent_ids
538 cur_iter = 0; max_iter = 100; # avoid infinite loop
539 while (parent_ids and (cur_iter < max_iter)):
541 messages_temp = message_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
542 parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
543 child_ids += parent_ids
544 if (cur_iter > max_iter):
545 _logger.warning("Possible infinite loop in _message_search_ancestor_ids. "\
546 "Note that this algorithm is intended to check for cycle in "\
547 "message graph, leading to a curious error. Have fun.")
550 def message_search_get_domain(self, cr, uid, ids, context=None):
551 """ OpenChatter feature: get the domain to search the messages related
552 to a document. mail.thread defines the default behavior as
553 being messages with model = self._name, id in ids.
554 This method should be overridden if a model has to implement a
557 return ['&', ('res_id', 'in', ids), ('model', '=', self._name)]
559 def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
560 limit=100, offset=0, domain=None, count=False, context=None):
561 """ OpenChatter feature: return thread messages ids according to the
562 search domain given by ``message_search_get_domain``.
564 It is possible to add in the search the parent of messages by
565 setting the fetch_ancestors flag to True. In that case, using
566 the parent_id relationship, the method returns the id list according
567 to the search domain, but then calls ``_message_search_ancestor_ids``
568 that will add to the list the ancestors ids. The search is limited
569 to parent messages having an id in ancestor_ids or having
570 parent_id set to False.
572 If ``count==True``, the number of ids is returned instead of the
573 id list. The count is done by hand instead of passing it as an
574 argument to the search call because we might want to perform
575 a research including parent messages until some ancestor_ids.
577 :param fetch_ancestors: performs an ascended search; will add
578 to fetched msgs all their parents until
580 :param ancestor_ids: used when fetching ancestors
581 :param domain: domain to add to the search; especially child_of
582 is interesting when dealing with threaded display.
583 Note that the added domain is anded with the
585 :param limit, offset, count, context: as usual
587 search_domain = self.message_search_get_domain(cr, uid, ids, context=context)
589 search_domain += domain
590 message_obj = self.pool.get('mail.message')
591 message_res = message_obj.search(cr, uid, search_domain, limit=limit, offset=offset, count=count, context=context)
592 if not count and fetch_ancestors:
593 message_res += self._message_search_ancestor_ids(cr, uid, ids, message_res, ancestor_ids, context=context)
596 def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
597 limit=100, offset=0, domain=None, context=None):
598 """ OpenChatter feature: read the messages related to some threads.
599 This method is used mainly the Chatter widget, to directly have
600 read result instead of searching then reading.
602 Please see message_search for more information about the parameters.
604 message_ids = self.message_search(cr, uid, ids, fetch_ancestors, ancestor_ids,
605 limit, offset, domain, context=context)
606 messages = self.pool.get('mail.message').read(cr, uid, message_ids, context=context)
608 """ Retrieve all attachments names """
609 map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
611 ids = map_id_to_name.keys()
612 names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
614 # convert the list of tuples into a dictionnary
616 map_id_to_name[name[0]] = name[1]
618 # give corresponding ids and names to each message
620 msg["attachments"] = []
622 for attach_id in msg["attachment_ids"]:
623 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
625 # Set the threads as read
626 self.message_check_and_set_read(cr, uid, ids, context=context)
627 # Sort and return the messages
628 messages = sorted(messages, key=lambda d: (-d['id']))
631 def message_get_pushed_messages(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
632 limit=100, offset=0, msg_search_domain=[], context=None):
633 """ OpenChatter: wall: get the pushed notifications and used them
634 to fetch messages to display on the wall.
636 :param fetch_ancestors: performs an ascended search; will add
637 to fetched msgs all their parents until
639 :param ancestor_ids: used when fetching ancestors
640 :param domain: domain to add to the search; especially child_of
641 is interesting when dealing with threaded display
642 :param ascent: performs an ascended search; will add to fetched msgs
643 all their parents until root_ids
644 :param root_ids: for ascent search
645 :return: list of mail.messages sorted by date
647 notification_obj = self.pool.get('mail.notification')
648 msg_obj = self.pool.get('mail.message')
649 # update message search
650 for arg in msg_search_domain:
651 if isinstance(arg, (tuple, list)):
652 arg[0] = 'message_id.' + arg[0]
653 # compose final domain
654 domain = [('user_id', '=', uid)] + msg_search_domain
656 notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
657 notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
658 msg_ids = [notification.message_id.id for notification in notifications]
660 msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
661 if (fetch_ancestors): msg_ids = self._message_search_ancestor_ids(cr, uid, ids, msg_ids, ancestor_ids, context=context)
662 msgs = msg_obj.read(cr, uid, msg_ids, context=context)
665 def _message_find_user_id(self, cr, uid, message, context=None):
666 from_local_part = to_email(decode(message.get('From')))[0]
667 user_ids = self.pool.get('res.users').search(cr, uid, [('login', '=', from_local_part)], context=context)
668 return user_ids[0] if user_ids else uid
670 #------------------------------------------------------
672 #------------------------------------------------------
673 # message_process will call either message_new or message_update.
675 def message_route(self, cr, uid, message, model=None, thread_id=None,
676 custom_values=None, context=None):
677 """Attempt to figure out the correct target model, thread_id,
678 custom_values and user_id to use for an incoming message.
679 Multiple values may be returned, if a message had multiple
680 recipients matching existing mail.aliases, for example.
682 The following heuristics are used, in this order:
683 1. If the message replies to an existing thread_id, and
684 properly contains the thread model in the 'In-Reply-To'
685 header, use this model/thread_id pair, and ignore
686 custom_value (not needed as no creation will take place)
687 2. Look for a mail.alias entry matching the message
688 recipient, and use the corresponding model, thread_id,
689 custom_values and user_id.
690 3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
692 4. If all the above fails, raise an exception.
694 :param string message: an email.message instance
695 :param string model: the fallback model to use if the message
696 does not match any of the currently configured mail aliases
697 (may be None if a matching alias is supposed to be present)
698 :type dict custom_values: optional dictionary of default field values
699 to pass to ``message_new`` if a new record needs to be created.
700 Ignored if the thread record already exists, and also if a
701 matching mail.alias was found (aliases define their own defaults)
702 :param int thread_id: optional ID of the record/thread from ``model``
703 to which this mail should be attached. Only used if the message
704 does not reply to an existing thread and does not match any mail alias.
705 :return: list of [model, thread_id, custom_values, user_id]
707 assert isinstance(message, Message), 'message must be an email.message.Message at this point'
708 message_id = message.get('Message-Id')
710 # 1. Verify if this is a reply to an existing thread
711 references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To')
712 ref_match = references and tools.reference_re.search(references)
714 thread_id = int(ref_match.group(1))
715 model = ref_match.group(2) or model
716 model_pool = self.pool.get(model)
717 if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
718 and hasattr(model_pool, 'message_update'):
719 _logger.debug('Routing mail with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
720 message_id, model, thread_id, custom_values, uid)
721 return [(model, thread_id, custom_values, uid)]
723 # 2. Look for a matching mail.alias entry
724 # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
725 # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
726 rcpt_tos = decode_header(message, 'Delivered-To') or \
727 ','.join([decode_header(message, 'To'),
728 decode_header(message, 'Cc'),
729 decode_header(message, 'Resent-To'),
730 decode_header(message, 'Resent-Cc')])
731 local_parts = [e.split('@')[0] for e in to_email(rcpt_tos)]
733 mail_alias = self.pool.get('mail.alias')
734 alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
737 for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
738 user_id = alias.alias_user_id.id
740 user_id = self._message_find_user_id(cr, uid, message, context=context)
741 routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
742 eval(alias.alias_defaults), user_id))
743 _logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
746 # 3. Fallback to the provided parameters, if they work
747 model_pool = self.pool.get(model)
749 # Legacy: fallback to matching [ID] in the Subject
750 match = tools.res_re.search(decode_header(message, 'Subject'))
751 thread_id = match and match.group(1)
752 assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
753 "No possible route found for incoming message with Message-Id %s. " \
754 "Create an appropriate mail.alias or force the destination model." % message_id
755 if thread_id and not model_pool.exists(cr, uid, thread_id):
756 _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
757 thread_id, message_id)
759 _logger.debug('Routing mail with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
760 message_id, model, thread_id, custom_values, uid)
761 return [(model, thread_id, custom_values, uid)]
764 def message_process(self, cr, uid, model, message, custom_values=None,
765 save_original=False, strip_attachments=False,
766 thread_id=None, context=None):
767 """Process an incoming RFC2822 email message, relying on
768 ``mail.message.parse()`` for the parsing operation,
769 and ``message_route()`` to figure out the target model.
771 Once the target model is known, its ``message_new`` method
772 is called with the new message (if the thread record did not exist)
773 or its ``message_update`` method (if it did). Finally,
774 ``message_forward`` is called to automatically notify other
775 people that should receive this message.
777 :param string model: the fallback model to use if the message
778 does not match any of the currently configured mail aliases
779 (may be None if a matching alias is supposed to be present)
780 :param message: source of the RFC2822 message
781 :type message: string or xmlrpclib.Binary
782 :type dict custom_values: optional dictionary of field values
783 to pass to ``message_new`` if a new record needs to be created.
784 Ignored if the thread record already exists, and also if a
785 matching mail.alias was found (aliases define their own defaults)
786 :param bool save_original: whether to keep a copy of the original
787 email source attached to the message after it is imported.
788 :param bool strip_attachments: whether to strip all attachments
789 before processing the message, in order to save some space.
790 :param int thread_id: optional ID of the record/thread from ``model``
791 to which this mail should be attached. When provided, this
792 overrides the automatic detection based on the message
795 if context is None: context = {}
797 # extract message bytes - we are forced to pass the message as binary because
798 # we don't know its encoding until we parse its headers and hence can't
799 # convert it to utf-8 for transport between the mailgate script and here.
800 if isinstance(message, xmlrpclib.Binary):
801 message = str(message.data)
802 # Warning: message_from_string doesn't always work correctly on unicode,
803 # we must use utf-8 strings here :-(
804 if isinstance(message, unicode):
805 message = message.encode('utf-8')
806 msg_txt = email.message_from_string(message)
807 routes = self.message_route(cr, uid, msg_txt, model,
808 thread_id, custom_values,
810 msg = self.pool.get('mail.message').parse_message(msg_txt, save_original=save_original, context=context)
811 msg['state'] = 'received'
812 if strip_attachments and 'attachments' in msg:
813 del msg['attachments']
814 for model, thread_id, custom_values, user_id in routes:
815 if self._name != model:
816 context.update({'thread_model': model})
817 model_pool = self.pool.get(model)
818 assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
819 "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
820 (msg['message-id'], model)
821 if thread_id and hasattr(model_pool, 'message_update'):
822 model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
824 thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
826 # Forward the email to other followers
827 self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context)
828 model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
831 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
832 """Called by ``message_process`` when a new message is received
833 for a given thread model, if the message did not belong to
835 The default behavior is to create a new record of the corresponding
836 model (based on some very basic info extracted from the message),
837 then attach the message to the newly created record
838 (by calling ``message_append_dict``).
839 Additional behavior may be implemented by overriding this method.
841 :param dict msg_dict: a map containing the email details and
842 attachments. See ``message_process`` and
843 ``mail.message.parse`` for details.
844 :param dict custom_values: optional dictionary of additional
845 field values to pass to create()
846 when creating the new thread record.
847 Be careful, these values may override
848 any other values coming from the message.
849 :param dict context: if a ``thread_model`` value is present
850 in the context, its value will be used
851 to determine the model of the record
852 to create (instead of the current model).
854 :return: the id of the newly created thread object
858 model = context.get('thread_model') or self._name
859 model_pool = self.pool.get(model)
860 fields = model_pool.fields_get(cr, uid, context=context)
861 data = model_pool.default_get(cr, uid, fields, context=context)
862 if 'name' in fields and not data.get('name'):
863 data['name'] = msg_dict.get('subject', '')
864 if custom_values and isinstance(custom_values, dict):
865 data.update(custom_values)
866 res_id = model_pool.create(cr, uid, data, context=context)
867 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
870 def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
871 """Called by ``message_process`` when a new message is received
872 for an existing thread. The default behavior is to create a
873 new mail.message in the given thread (by calling
874 ``message_append_dict``)
875 Additional behavior may be implemented by overriding this
877 :param dict msg_dict: a map containing the email details and
878 attachments. See ``message_process`` and
879 ``mail.message.parse()`` for details.
880 :param dict update_vals: a dict containing values to update records
881 given their ids; if the dict is None or is
882 void, no write operation is performed.
885 self.write(cr, uid, ids, update_vals, context=context)
886 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
888 def message_thread_followers(self, cr, uid, ids, context=None):
889 """ Returns a list of email addresses of the people following
890 this thread, including the sender of each mail, and the
891 people who were in CC of the messages, if any.
894 if isinstance(ids, (str, int, long)):
896 for thread in self.browse(cr, uid, ids, context=context):
898 for message in thread.message_ids:
899 l.add((message.user_id and message.user_id.email) or '')
900 l.add(message.email_from or '')
901 l.add(message.email_cc or '')
902 res[thread.id] = filter(None, l)
905 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
906 """Sends an email to all people following the given threads.
907 The emails are forwarded immediately, not queued for sending,
910 :param str model: thread model
911 :param list thread_ids: ids of the thread records
912 :param msg: email.message.Message object to forward
913 :param email_error: optional email address to notify in case
914 of any delivery error during the forward.
917 model_pool = self.pool.get(model)
918 smtp_server_obj = self.pool.get('ir.mail_server')
919 for res in model_pool.browse(cr, uid, thread_ids, context=context):
920 if hasattr(model_pool, 'message_thread_followers'):
921 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
923 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
924 message_followers_emails = to_email(','.join(filter(None, followers)))
925 message_recipients = to_email(','.join(filter(None,
926 [decode(msg['from']),
928 decode(msg['cc'])])))
929 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
931 # TODO: we need an interface for this for all types of objects, not just leads
932 if model_pool._columns.get('section_id'):
934 msg['reply-to'] = res.section_id.reply_to
936 smtp_from, = to_email(msg['from'])
937 msg['from'] = smtp_from
938 msg['to'] = ", ".join(forward_to)
939 msg['message-id'] = tools.generate_tracking_message_id(res.id)
940 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
941 subj = msg['subject']
942 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
943 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
944 msg['to'] = email_error
945 smtp_server_obj.send_email(cr, uid, msg)
948 def message_partner_by_email(self, cr, uid, email, context=None):
949 """Attempts to return the id of a partner address matching
950 the given ``email``, and the corresponding partner id.
951 Can be used by classes using the ``mail.thread`` mixin
952 to lookup the partner and use it in their implementation
953 of ``message_new`` to link the new record with a
954 corresponding partner.
955 The keys used in the returned dict are meant to map
956 to usual names for relationships towards a partner
957 and one of its addresses.
959 :param email: email address for which a partner
960 should be searched for.
962 :return: a map of the following form::
964 { 'partner_address_id': id or False,
965 'partner_id': pid or False }
967 partner_pool = self.pool.get('res.partner')
968 res = {'partner_id': False}
970 email = to_email(email)[0]
971 contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
973 contact = partner_pool.browse(cr, uid, contact_ids[0])
974 res['partner_id'] = contact.id
977 # for backwards-compatibility with old scripts
978 process_email = message_process
980 #------------------------------------------------------
982 #------------------------------------------------------
984 def log(self, cr, uid, id, message, secondary=False, context=None):
985 _logger.warning("log() is deprecated. As this module inherit from \
986 mail.thread, the message will be managed by this \
987 module instead of by the res.log mechanism. Please \
988 use the mail.thread OpenChatter API instead of the \
989 now deprecated res.log.")
990 self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
992 def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False,
993 type='notification', content_subtype='html', context=None):
994 if content_subtype == 'html':
1000 return self.message_append(cr, uid, ids, subject, body_html, body_text,
1001 type, parent_id=parent_id,
1002 content_subtype=content_subtype, context=context)
1004 #------------------------------------------------------
1005 # Subscription mechanism
1006 #------------------------------------------------------
1008 def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
1009 """ Returns a list of fields containing a res.user.id. Those fields
1010 will be checked to automatically subscribe those users.
1014 def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
1015 """ Subscribe the user (or user_ids) to the current document.
1017 :param user_ids: a list of user_ids; if not set, subscribe
1019 :param return: new value of followers, for Chatter
1021 to_subscribe_uids = [uid] if user_ids is None else user_ids
1022 write_res = self.write(cr, uid, ids, {'message_follower_ids': self.message_subscribe_get_command(cr, uid, to_subscribe_uids, context)}, context=context)
1023 return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
1025 def message_subscribe_get_command(self, cr, uid, follower_ids, context=None):
1026 """ Generate the many2many command to add followers. """
1027 return [(4, id) for id in follower_ids]
1029 def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
1030 """ Unsubscribe the user (or user_ids) from the current document.
1032 :param user_ids: a list of user_ids; if not set, subscribe
1034 :param return: new value of followers, for Chatter
1036 to_unsubscribe_uids = [uid] if user_ids is None else user_ids
1037 write_res = self.write(cr, uid, ids, {'message_follower_ids': self.message_unsubscribe_get_command(cr, uid, to_unsubscribe_uids, context)}, context=context)
1038 return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
1040 def message_unsubscribe_get_command(self, cr, uid, follower_ids, context=None):
1041 """ Generate the many2many command to remove followers. """
1042 return [(3, id) for id in follower_ids]
1044 #------------------------------------------------------
1046 #------------------------------------------------------
1048 def message_create_notify_by_email(self, cr, uid, new_msg_values, user_to_notify_ids, context=None):
1049 """ When creating a new message and pushing notifications, emails
1050 must be send if users have chosen to receive notifications
1051 by email via the notification_email_pref field.
1053 ``notification_email_pref`` can have 3 values :
1054 - all: receive all notification by email (for example for shared
1056 - to_me: messages send directly to me (@login, messages on res.users)
1057 - never: never receive notifications
1058 Note that an user should never receive notifications for messages
1061 :param new_msg_values: dictionary of message values, those that
1062 are given to the create method
1063 :param user_to_notify_ids: list of user_ids, user that will
1064 receive a notification on their Wall
1066 message_obj = self.pool.get('mail.message')
1067 res_users_obj = self.pool.get('res.users')
1068 body = new_msg_values.get('body_html', '') if new_msg_values.get('content_subtype') == 'html' else new_msg_values.get('body_text', '')
1070 # remove message writer
1071 if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0:
1072 user_to_notify_ids.remove(new_msg_values.get('user_id'))
1074 # try to find an email_to
1076 for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context):
1077 # TO BE REFACTORED BY FP, JUSTE REMOVED TO_ME, NOT SURE WHAT S NEW BEHAVIOR
1078 if not user.notification_email_pref == 'all':
1082 email_to = '%s, %s' % (email_to, user.email)
1083 email_to = email_to.lstrip(', ')
1085 # did not find any email address: not necessary to create an email
1089 # try to find an email_from
1090 current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
1091 email_from = new_msg_values.get('email_from')
1093 email_from = current_user.email
1095 # get email content, create it (with mail_message.create)
1096 email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
1097 email_id = message_obj.create(cr, uid, email_values, context=context)
1100 def message_create_notify_get_email_dict(self, cr, uid, new_msg_values, email_from, email_to, context=None):
1101 values = dict(new_msg_values)
1103 body_html = new_msg_values.get('body_html', '')
1105 body_html += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1106 body_text = new_msg_values.get('body_text', '')
1108 body_text += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
1111 'state': 'outgoing',
1112 'email_from': email_from,
1113 'email_to': email_to,
1114 'subject': 'New message',
1115 'content_subtype': new_msg_values.get('content_subtype', 'plain'),
1116 'body_html': body_html,
1117 'body_text': body_text,
1118 'auto_delete': True,
1124 def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
1125 notif_obj = self.pool.get('mail.notification')
1126 msg_obj = self.pool.get('mail.message')
1128 notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
1130 notif_msg_ids = msg_ids
1131 to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
1132 return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
1134 #------------------------------------------------------
1136 #------------------------------------------------------
1138 def message_create_set_unread(self, cr, uid, ids, context=None):
1139 """ When creating a new message, set as unread if uid is not the
1140 object responsible. """
1141 for obj in self.browse(cr, uid, ids, context=context):
1142 if obj.message_state and self._columns.get('user_id') and (not obj.user_id or obj.user_id.id != uid):
1143 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1145 def message_check_and_set_unread(self, cr, uid, ids, context=None):
1146 """ Set unread if uid is the object responsible or if the object has
1148 for obj in self.browse(cr, uid, ids, context=context):
1149 if obj.message_state and self._columns.get('user_id') and (not obj.user_id or obj.user_id.id == uid):
1150 self.message_mark_as_unread(cr, uid, [obj.id], context=context)
1152 def message_mark_as_unread(self, cr, uid, ids, context=None):
1153 """ Set as unread. """
1154 return self.write(cr, uid, ids, {'message_state': False}, context=context)
1156 def message_check_and_set_read(self, cr, uid, ids, context=None):
1157 """ Set read if uid is the object responsible. """
1158 for obj in self.browse(cr, uid, ids, context=context):
1159 if not obj.message_state and self._columns.get('user_id') and obj.user_id and obj.user_id.id == uid:
1160 self.message_mark_as_read(cr, uid, [obj.id], context=context)
1162 def message_mark_as_read(self, cr, uid, ids, context=None):
1163 """ Set as read. """
1164 return self.write(cr, uid, ids, {'message_state': True}, context=context)
1167 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: