[Merge] Merge with main addons.
[odoo/odoo.git] / addons / mail / mail_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
6 #
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
11 #
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
16 #
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/>
19 #
20 ##############################################################################
21
22 import logging
23 import tools
24
25 from email.header import decode_header
26 from openerp import SUPERUSER_ID
27 from osv import osv, orm, fields
28 from tools.translate import _
29
30 _logger = logging.getLogger(__name__)
31
32 """ Some tools for parsing / creating email fields """
33 def decode(text):
34     """Returns unicode() string conversion of the the given encoded smtp header text"""
35     if text:
36         text = decode_header(text.replace('\r', ''))
37         return ''.join([tools.ustr(x[0], x[1]) for x in text])
38
39
40 class mail_message(osv.Model):
41     """ Messages model: system notification (replacing res.log notifications),
42         comments (OpenChatter discussion) and incoming emails. """
43     _name = 'mail.message'
44     _description = 'Message'
45     _inherit = ['ir.needaction_mixin']
46     _order = 'id desc'
47
48     _message_read_limit = 10
49     _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read',
50         'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids']
51     _message_record_name_length = 18
52     _message_read_more_limit = 1024
53
54     def _shorten_name(self, name):
55         if len(name) <= (self._message_record_name_length + 3):
56             return name
57         return name[:self._message_record_name_length] + '...'
58
59     def _get_record_name(self, cr, uid, ids, name, arg, context=None):
60         """ Return the related document name, using name_get. It is included in
61             a try/except statement, because if uid cannot read the related
62             document, he should see a void string instead of crashing. """
63         result = dict.fromkeys(ids, False)
64         for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
65             if not message['model'] or not message['res_id']:
66                 continue
67             try:
68                 result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, uid, [message['res_id']], context=context)[0][1])
69             except (orm.except_orm, osv.except_osv):
70                 pass
71         return result
72
73     def _get_to_read(self, cr, uid, ids, name, arg, context=None):
74         """ Compute if the message is unread by the current user. """
75         res = dict((id, False) for id in ids)
76         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
77         notif_obj = self.pool.get('mail.notification')
78         notif_ids = notif_obj.search(cr, uid, [
79             ('partner_id', 'in', [partner_id]),
80             ('message_id', 'in', ids),
81             ('read', '=', False)
82         ], context=context)
83         for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
84             res[notif.message_id.id] = not notif.read
85         return res
86
87     def _search_to_read(self, cr, uid, obj, name, domain, context=None):
88         """ Search for messages to read by the current user. Condition is
89             inversed because we search unread message on a read column. """
90         if domain[0][2]:
91             read_cond = "(read = False OR read IS NULL)"
92         else:
93             read_cond = "read = True"
94         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
95         cr.execute("SELECT message_id FROM mail_notification "\
96                         "WHERE partner_id = %%s AND %s" % read_cond,
97                     (partner_id,))
98         return [('id', 'in', [r[0] for r in cr.fetchall()])]
99
100     def name_get(self, cr, uid, ids, context=None):
101         # name_get may receive int id instead of an id list
102         if isinstance(ids, (int, long)):
103             ids = [ids]
104         res = []
105         for message in self.browse(cr, uid, ids, context=context):
106             name = '%s: %s' % (message.subject or '', message.body or '')
107             res.append((message.id, self._shorten_name(name.lstrip(' :'))))
108         return res
109
110     _columns = {
111         'type': fields.selection([
112                         ('email', 'Email'),
113                         ('comment', 'Comment'),
114                         ('notification', 'System notification'),
115                         ], 'Type',
116             help="Message type: email for email message, notification for system "\
117                  "message, comment for other messages such as user replies"),
118         'author_id': fields.many2one('res.partner', 'Author', required=True),
119         'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
120         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
121             'message_id', 'attachment_id', 'Attachments'),
122         'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
123         'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
124         'model': fields.char('Related Document Model', size=128, select=1),
125         'res_id': fields.integer('Related Document ID', select=1),
126         'record_name': fields.function(_get_record_name, type='string',
127             string='Message Record Name',
128             help="Name get of the related document."),
129         'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
130         'subject': fields.char('Subject'),
131         'date': fields.datetime('Date'),
132         'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
133         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
134         'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
135             type='boolean', string='To read',
136             help='Functional field to search for messages the current user has to read'),
137         'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
138         'vote_user_ids': fields.many2many('res.users', 'mail_vote',
139             'message_id', 'user_id', string='Votes',
140             help='Users that voted for this message'),
141         'favorite_user_ids': fields.many2many('res.users', 'mail_favorite',
142             'message_id', 'user_id', string='Favorite',
143             help='Users that set this message in their favorites'),
144     }
145
146     def _needaction_domain_get(self, cr, uid, context=None):
147         if self._needaction:
148             return [('to_read', '=', True)]
149         return []
150
151     def _get_default_author(self, cr, uid, context=None):
152         return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
153
154     _defaults = {
155         'type': 'email',
156         'date': lambda *a: fields.datetime.now(),
157         'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
158         'body': '',
159     }
160
161     #------------------------------------------------------
162     # Vote/Like
163     #------------------------------------------------------
164
165     def vote_toggle(self, cr, uid, ids, context=None):
166         ''' Toggles vote. Performed using read to avoid access rights issues.
167             Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
168         for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
169             new_has_voted = not (uid in message.get('vote_user_ids'))
170             if new_has_voted:
171                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
172             else:
173                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
174         return new_has_voted or False
175
176     #------------------------------------------------------
177     # Favorite
178     #------------------------------------------------------
179
180     def favorite_toggle(self, cr, uid, ids, context=None):
181         ''' Toggles favorite. Performed using read to avoid access rights issues.
182             Done as SUPERUSER_ID because uid may star a message he cannot modify. '''
183         for message in self.read(cr, uid, ids, ['favorite_user_ids'], context=context):
184             new_is_favorite = not (uid in message.get('favorite_user_ids'))
185             if new_is_favorite:
186                 self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(4, uid)]}, context=context)
187             else:
188                 self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(3, uid)]}, context=context)
189         return new_is_favorite or False
190
191     #------------------------------------------------------
192     # Message loading for web interface
193     #------------------------------------------------------
194
195     def _message_get_dict(self, cr, uid, message, context=None):
196         """ Return a dict representation of the message. This representation is
197             used in the JS client code, to display the messages.
198
199             :param dict message: read result of a mail.message
200         """
201         has_voted = False
202         if uid in message['vote_user_ids']:
203             has_voted = True
204
205         is_favorite = False
206         if uid in message['favorite_user_ids']:
207             is_favorite = True
208
209         try:
210             attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)]
211         except (orm.except_orm, osv.except_osv):
212             attachment_ids = []
213
214         try:
215             partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context)
216         except (orm.except_orm, osv.except_osv):
217             partner_ids = []
218
219         return {
220             'id': message['id'],
221             'type': message['type'],
222             'attachment_ids': attachment_ids,
223             'body': message['body'],
224             'model': message['model'],
225             'res_id': message['res_id'],
226             'record_name': message['record_name'],
227             'subject': message['subject'],
228             'date': message['date'],
229             'author_id': message['author_id'],
230             'is_author': message['author_id'] and message['author_id'][0] == uid,
231             # TDE note: is this useful ? to check
232             'partner_ids': partner_ids,
233             'parent_id': message['parent_id'] and message['parent_id'][0] or False,
234             # TDE note: see with CHM about votes, how they are displayed (only number, or name_get ?)
235             # vote: should only use number of votes
236             'vote_nb': len(message['vote_user_ids']),
237             'has_voted': has_voted,
238             'is_private': message['model'] and message['res_id'],
239             'is_favorite': is_favorite,
240             'to_read': message['to_read'],
241         }
242
243     def _message_read_expandable(self, cr, uid, message_list, read_messages,
244             message_loaded_ids=[], domain=[], context=None, parent_id=False, limit=None):
245         """ Create the expandable message for all parent message read
246             this function is used by message_read
247
248             :param list message_list: list of messages given by message_read to
249                 which we have to add expandables
250             :param dict read_messages: dict [id]: read result of the messages to
251                 easily have access to their values, given their ID
252         """
253         # sort for group items / TDE: move to message_read
254         # result = sorted(result, key=lambda k: k['id'])
255         tree_not = []
256         # expandable for not show message
257         id_list = sorted(read_messages.keys())
258         for message_id in id_list:
259             message = read_messages[message_id]
260
261             # TDE note: check search is correctly implemented in mail.message
262             not_loaded_ids = self.search(cr, uid, [
263                 ('parent_id', '=', message['id']),
264                 ('id', 'not in', message_loaded_ids),
265                 ], context=context, limit=self._message_read_more_limit)
266             # group childs not read
267             id_min = None
268             id_max = None
269             nb = 0
270
271             for not_loaded_id in not_loaded_ids:
272                 if not read_messages.get(not_loaded_id):
273                     nb += 1
274                     if id_min == None or id_min > not_loaded_id:
275                         id_min = not_loaded_id
276                     if id_max == None or id_max < not_loaded_id:
277                         id_max = not_loaded_id
278                     tree_not.append(not_loaded_id)
279                 else:
280                     if nb > 0:
281                         message_list.append({
282                             'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', message_id)],
283                             'nb_messages': nb,
284                             'type': 'expandable',
285                             'parent_id': message_id,
286                             'id':  id_min,
287                             'model':  message['model']
288                         })
289                     id_min = None
290                     id_max = None
291                     nb = 0
292             if nb > 0:
293                 message_list.append({
294                     'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', message_id)],
295                     'nb_messages': nb,
296                     'type': 'expandable',
297                     'parent_id': message_id,
298                     'id':  id_min,
299                     'model':  message['model'],
300                 })
301
302         for msg_id in read_messages.keys() + tree_not:
303             message_loaded_ids.append(msg_id)
304
305         # expandable for limit max
306         ids = self.search(cr, uid, domain + [('id', 'not in', message_loaded_ids)], context=context, limit=1)
307         if len(ids) > 0:
308             message_list.append({
309                 'domain': domain,
310                 'nb_messages': 0,
311                 'type': 'expandable',
312                 'parent_id': parent_id,
313                 'id': -1,
314                 'max_limit': True,
315             })
316
317         return message_list
318
319     def _get_parent(self, cr, uid, message, context=None):
320         """ Tools method that tries to get the parent of a mail.message. If
321             no parent, or if uid has no access right on the parent, False
322             is returned.
323
324             :param dict message: read result of a mail.message
325         """
326         if not message['parent_id']:
327             return False
328         parent_id = message['parent_id'][0]
329         try:
330             return self.read(cr, uid, parent_id, self._message_read_fields, context=context)
331         except (orm.except_orm, osv.except_osv):
332             return False
333
334     def message_read(self, cr, uid, ids=False, domain=[], message_loaded_ids=[], context=None, parent_id=False, limit=None):
335         """ Read messages from mail.message, and get back a structured tree
336             of messages to be displayed as discussion threads. If IDs is set,
337             fetch these records. Otherwise use the domain to fetch messages.
338             After having fetch messages, their parents & child will be added to obtain
339             well formed threads.
340
341             TDE note: update this comment after final method implementation
342
343             :param domain: optional domain for searching ids
344             :param limit: number of messages to fetch
345             :param parent_id: if parent_id reached, stop searching for
346                 further parents
347             :return list: list of trees of messages
348         """
349         if message_loaded_ids:
350             domain += [('id', 'not in', message_loaded_ids)]
351         limit = limit or self._message_read_limit
352         read_messages = {}
353         message_list = []
354
355         # specific IDs given: fetch those ids and return directly the message list
356         if ids:
357             for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
358                 message_list.append(self._message_get_dict(cr, uid, message, context=context))
359             message_list = sorted(message_list, key=lambda k: k['id'])
360             return message_list
361
362         # TDE FIXME: check access rights on search are implemented for mail.message
363         # fetch messages according to the domain, add their parents if uid has access to
364         ids = self.search(cr, uid, domain, context=context, limit=limit)
365         for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
366             # if not in tree and not in message_loded list
367             if not read_messages.get(message.get('id')) and message.get('id') not in message_loaded_ids:
368                 read_messages[message.get('id')] = message
369                 message_list.append(self._message_get_dict(cr, uid, message, context=context))
370
371                 # get all parented message if the user have the access
372                 parent = self._get_parent(cr, uid, message, context=context)
373                 while parent and parent.get('id') != parent_id:
374                     if not read_messages.get(parent.get('id')) and parent.get('id') not in message_loaded_ids:
375                         read_messages[parent.get('id')] = parent
376                         message_list.append(self._message_get_dict(cr, uid, parent, context=context))
377                     parent = self._get_parent(cr, uid, parent, context=context)
378
379         # get the child expandable messages for the tree
380         message_list = sorted(message_list, key=lambda k: k['id'])
381         message_list = self._message_read_expandable(cr, uid, message_list, read_messages,
382             message_loaded_ids=message_loaded_ids, domain=domain, context=context, parent_id=parent_id, limit=limit)
383
384         # message_list = sorted(message_list, key=lambda k: k['id'])
385         return message_list
386
387     # TDE Note: do we need this ?
388     # def user_free_attachment(self, cr, uid, context=None):
389     #     attachment = self.pool.get('ir.attachment')
390     #     attachment_list = []
391     #     attachment_ids = attachment.search(cr, uid, [('res_model', '=', 'mail.message'), ('create_uid', '=', uid)])
392     #     if len(attachment_ids):
393     #         attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)]
394     #     return attachment_list
395
396     #------------------------------------------------------
397     # Email api
398     #------------------------------------------------------
399
400     def init(self, cr):
401         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
402         if not cr.fetchone():
403             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
404
405     def check_access_rule(self, cr, uid, ids, operation, context=None):
406         """ Access rules of mail.message:
407             - read: if
408                 - notification exist (I receive pushed message) OR
409                 - author_id = pid (I am the author) OR
410                 - I can read the related document if res_model, res_id
411                 - Otherwise: raise
412             - create: if
413                 - I am in the document message_follower_ids OR
414                 - I can write on the related document if res_model, res_id OR
415                 - I create a private message (no model, no res_id)
416                 - Otherwise: raise
417             - write: if
418                 - I can write on the related document if res_model, res_id
419                 - Otherwise: raise
420             - unlink: if
421                 - I can write on the related document if res_model, res_id
422                 - Otherwise: raise
423         """
424         if uid == SUPERUSER_ID:
425             return
426         if isinstance(ids, (int, long)):
427             ids = [ids]
428         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
429
430         # Read mail_message.ids to have their values
431         model_record_ids = {}
432         message_values = dict.fromkeys(ids)
433         cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
434         for id, rmod, rid, author_id in cr.fetchall():
435             message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
436             if rmod:
437                 model_record_ids.setdefault(rmod, set()).add(rid)
438
439         # Read: Check for received notifications -> could become an ir.rule, but not till we do not have a many2one variable field
440         if operation == 'read':
441             not_obj = self.pool.get('mail.notification')
442             not_ids = not_obj.search(cr, SUPERUSER_ID, [
443                 ('partner_id', '=', partner_id),
444                 ('message_id', 'in', ids),
445             ], context=context)
446             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
447         else:
448             notified_ids = []
449         # Read: Check messages you are author -> could become an ir.rule, but not till we do not have a many2one variable field
450         if operation == 'read':
451             author_ids = [mid for mid, message in message_values.iteritems()
452                 if message.get('author_id') and message.get('author_id') == partner_id]
453         # Create: Check messages you create that are private messages -> ir.rule ?
454         elif operation == 'create':
455             author_ids = [mid for mid, message in message_values.iteritems()
456                 if not message.get('model') and not message.get('res_id')]
457         else:
458             author_ids = []
459
460         # Create: Check message_follower_ids
461         if operation == 'create':
462             doc_follower_ids = []
463             for model, mids in model_record_ids.items():
464                 fol_obj = self.pool.get('mail.followers')
465                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
466                     ('res_model', '=', model),
467                     ('res_id', 'in', list(mids)),
468                     ('partner_id', '=', partner_id),
469                     ], context=context)
470                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
471                 doc_follower_ids += [mid for mid, message in message_values.iteritems()
472                     if message.get('res_model') == model and message.get('res_id') in fol_mids]
473         else:
474             doc_follower_ids = []
475
476         # Calculate remaining ids, and related model/res_ids
477         model_record_ids = {}
478         other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids))
479         for id in other_ids:
480             if message_values[id]['res_model']:
481                 model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
482
483         # CRUD: Access rights related to the document
484         document_related_ids = []
485         for model, mids in model_record_ids.items():
486             model_obj = self.pool.get(model)
487             mids = model_obj.exists(cr, uid, mids)
488             if operation in ['create', 'write', 'unlink']:
489                 model_obj.check_access_rights(cr, uid, 'write')
490                 model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
491             else:
492                 model_obj.check_access_rights(cr, uid, operation)
493                 model_obj.check_access_rule(cr, uid, mids, operation, context=context)
494             document_related_ids += [mid for mid, message in message_values.iteritems()
495                 if message.get('res_model') == model and message.get('res_id') in mids]
496
497         # Calculate remaining ids: if not void, raise an error
498         other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids), set(document_related_ids))
499         if not other_ids:
500             return
501         raise orm.except_orm(_('Access Denied'),
502                             _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
503                             (self._description, operation))
504
505     def create(self, cr, uid, values, context=None):
506         if not values.get('message_id') and values.get('res_id') and values.get('model'):
507             values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
508         newid = super(mail_message, self).create(cr, uid, values, context)
509         self._notify(cr, SUPERUSER_ID, newid, context=context)
510         return newid
511
512     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
513         """ Override to explicitely call check_access_rule, that is not called
514             by the ORM. It instead directly fetches ir.rules and apply them. """
515         self.check_access_rule(cr, uid, ids, 'read', context=context)
516         res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
517         return res
518
519     def unlink(self, cr, uid, ids, context=None):
520         # cascade-delete attachments that are directly attached to the message (should only happen
521         # for mail.messages that act as parent for a standalone mail.mail record).
522         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
523         attachments_to_delete = []
524         for message in self.browse(cr, uid, ids, context=context):
525             for attach in message.attachment_ids:
526                 if attach.res_model == self._name and attach.res_id == message.id:
527                     attachments_to_delete.append(attach.id)
528         if attachments_to_delete:
529             self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
530         return super(mail_message, self).unlink(cr, uid, ids, context=context)
531
532     def _notify_followers(self, cr, uid, newid, message, context=None):
533         """ Add the related record followers to the destination partner_ids.
534         """
535         partners_to_notify = set([])
536         # message has no subtype_id: pure log message -> no partners, no one notified
537         if not message.subtype_id:
538             message.write({'partner_ids': [5]})
539             return True
540         # all partner_ids of the mail.message have to be notified
541         if message.partner_ids:
542             partners_to_notify |= set(partner.id for partner in message.partner_ids)
543         # all followers of the mail.message document have to be added as partners and notified
544         if message.model and message.res_id:
545             fol_obj = self.pool.get("mail.followers")
546             fol_ids = fol_obj.search(cr, uid, [('res_model', '=', message.model), ('res_id', '=', message.res_id), ('subtype_ids', 'in', message.subtype_id.id)], context=context)
547             fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context)
548             extra_notified = set(fol.partner_id.id for fol in fol_objs)
549             missing_notified = extra_notified - partners_to_notify
550             if missing_notified:
551                 self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context)
552
553     def _notify(self, cr, uid, newid, context=None):
554         """ Add the related record followers to the destination partner_ids if is not a private message.
555             Call mail_notification.notify to manage the email sending
556         """
557         message = self.browse(cr, uid, newid, context=context)
558         if message.model and message.res_id:
559             self._notify_followers(cr, uid, newid, message, context=context)
560
561         # add myself if I wrote on my wall, otherwise remove myself author
562         if ((message.model == "res.partner" and message.res_id == message.author_id.id)):
563             self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, message.author_id.id)]}, context=context)
564         else:
565             self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, message.author_id.id)]}, context=context)
566
567         self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
568
569     def copy(self, cr, uid, id, default=None, context=None):
570         """Overridden to avoid duplicating fields that are unique to each email"""
571         if default is None:
572             default = {}
573         default.update(message_id=False, headers=False)
574         return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
575
576     #------------------------------------------------------
577     # Tools
578     #------------------------------------------------------
579
580     def check_partners_email(self, cr, uid, partner_ids, context=None):
581         """ Verify that selected partner_ids have an email_address defined.
582             Otherwise throw a warning. """
583         partner_wo_email_lst = []
584         for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
585             if not partner.email:
586                 partner_wo_email_lst.append(partner)
587         if not partner_wo_email_lst:
588             return {}
589         for partner in partner_wo_email_lst:
590             recipients = "contacts"
591             if partner.customer and not partner.supplier:
592                 recipients = "customers"
593             elif partner.supplier and not partner.customer:
594                 recipients = "suppliers"
595             warning_msg = _('The following %s do not have an email address specified.') % (recipients,)
596             warning_msg += '\n- %s' % (partner.name)
597         return {'warning': {
598                     'title': _('Email not found'),
599                     'message': warning_msg,
600                     }
601                 }