1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-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 ##############################################################################
23 from email.header import decode_header
24 from osv import osv, fields
27 _logger = logging.getLogger(__name__)
29 """ Some tools for parsing / creating email fields """
31 """Returns unicode() string conversion of the the given encoded smtp header text"""
33 text = decode_header(text.replace('\r', ''))
34 return ''.join([tools.ustr(x[0], x[1]) for x in text])
36 class mail_message(osv.Model):
37 """Model holding messages: system notification (replacing res.log
38 notifications), comments (for OpenChatter feature). This model also
39 provides facilities to parse new email messages. Type of messages are
40 differentiated using the 'type' column. """
42 _name = 'mail.message'
43 _description = 'Message'
44 _inherit = ['ir.needaction_mixin']
47 _message_read_limit = 10
48 _message_record_name_length = 18
50 def _shorten_name(self, name):
51 if len(name) <= (self._message_record_name_length+3):
53 return name[:self._message_record_name_length] + '...'
55 def _get_record_name(self, cr, uid, ids, name, arg, context=None):
56 """ Return the related document name, using get_name. """
57 result = dict.fromkeys(ids, '')
58 for message in self.browse(cr, uid, ids, context=context):
59 if not message.model or not message.res_id:
61 result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
64 def _get_unread(self, cr, uid, ids, name, arg, context=None):
65 """ Compute if the message is unread by the current user. """
66 res = dict((id, {'unread': False}) for id in ids)
67 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
68 notif_obj = self.pool.get('mail.notification')
69 notif_ids = notif_obj.search(cr, uid, [
70 ('partner_id', 'in', [partner_id]),
71 ('message_id', 'in', ids),
74 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
75 res[notif.message_id.id]['unread'] = True
78 def _search_unread(self, cr, uid, obj, name, domain, context=None):
79 """ Search for messages unread by the current user. """
80 read_value = not domain[0][2]
81 read_cond = '' if read_value else '!= true'
82 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
83 cr.execute(""" SELECT mail_message.id \
85 JOIN mail_notification ON ( \
86 mail_notification.message_id = mail_message.id ) \
87 WHERE mail_notification.partner_id = %%s AND \
88 mail_notification.read %s \
89 """ % read_cond, (partner_id,) )
91 message_ids = [r[0] for r in res]
92 return [('id', 'in', message_ids)]
95 def name_get(self, cr, uid, ids, context=None):
96 # name_get may receive int id instead of an id list
97 if isinstance(ids, (int, long)):
100 for message in self.browse(cr, uid, ids, context=context):
101 name = '%s: %s' % (message.subject or '', message.body or '')
102 res.append((message.id, self._shorten_name(name.lstrip(' :'))))
106 # should we keep a distinction between email and comment ?
107 'type': fields.selection([
109 ('comment', 'Comment'),
110 ('notification', 'System notification'),
112 help="Message type: email for email message, notification for system "\
113 "message, comment for other messages such as user replies"),
114 'author_id': fields.many2one('res.partner', 'Author', required=True),
115 'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
116 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
117 'message_id', 'attachment_id', 'Attachments'),
118 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
119 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
120 'model': fields.char('Related Document Model', size=128, select=1),
121 'res_id': fields.integer('Related Document ID', select=1),
122 'record_name': fields.function(_get_record_name, type='string',
123 string='Message Record Name',
124 help="Name get of the related document."),
125 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
126 'subject': fields.char('Subject'),
127 'date': fields.datetime('Date'),
128 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
129 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
130 'unread': fields.function(_get_unread, fnct_search=_search_unread,
131 type='boolean', string='Unread',
132 help='Functional field to search for unread messages linked to uid'),
133 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
136 def _needaction_domain_get(self, cr, uid, context=None):
138 return [('unread', '=', True)]
141 def _get_default_author(self, cr, uid, context=None):
142 return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
146 'date': lambda *a: fields.datetime.now(),
147 'author_id': _get_default_author
151 #------------------------------------------------------
152 # Message loading for web interface
153 #------------------------------------------------------
155 def _message_dict_get(self, cr, uid, msg, context=None):
156 """ Return a dict representation of the message browse record. """
157 attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
158 author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
159 author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
160 partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
164 'attachment_ids': attachment_ids,
167 'res_id': msg.res_id,
168 'record_name': msg.record_name,
169 'subject': msg.subject,
171 'author_id': author_id,
172 'author_user_id': author_user_id,
173 'partner_ids': partner_ids,
177 def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
178 """ Given a tree with several roots of following structure :
180 {'id': 1, 'child_ids':[
181 {'id': 11, 'child_ids': [...] },
185 Flatten it to have a maximum number of level, with 0 being
187 Perform the flattening at leafs if above the maximum depth, then get
190 def _flatten(msg_dict):
191 """ from {'id': x, 'child_ids': [{child1}, {child2}]}
192 get [{'id': x, 'child_ids': []}, {child1}, {child2}]
194 child_ids = msg_dict.pop('child_ids', [])
195 msg_dict['child_ids'] = []
196 return [msg_dict] + child_ids
197 # Depth-first flattening
198 for message in messages:
199 message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level+1, level, context=context)
200 # Flatten if above maximum depth
201 if current_level < level:
204 for x in range(0, len(messages)):
205 flatenned = _flatten(messages[x])
206 for flat in flatenned:
207 new_list.append(flat)
211 def _debug_print_tree(self, tree, prefix=''):
213 print '%s%s (%s childs: %s)' % (prefix, elem['id'], len(elem['child_ids']), [xelem['id'] for xelem in elem['child_ids']])
214 if elem['child_ids']:
215 self._debug_print_tree(elem['child_ids'], prefix+'--')
217 def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
219 If IDS are provided, fetch these records, otherwise use the domain to
220 fetch the matching records. After having fetched the records provided
221 by IDS, it will fetch children (according to thread_level).
227 limit = limit or self._message_read_limit
228 context = context or {}
230 ids = self.search(cr, uid, domain, context=context, limit=limit)
232 # FP Todo: flatten to max X level of mail_thread
233 messages = self.browse(cr, uid, ids, context=context)
236 tree = {} # key: ID, value: record
238 if len(result)<(limit-1):
239 record = self._message_dict_get(cr, uid, msg, context=context)
240 if thread_level and msg.parent_id:
242 if msg.parent_id.id in tree:
243 record_parent = tree[msg.parent_id.id]
245 record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
246 if msg.parent_id.parent_id:
247 tree[msg.parent_id.id] = record_parent
248 if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
249 record_parent['child_ids'].append(record)
250 record = record_parent
252 if msg.id not in tree:
253 result.append(record)
254 tree[msg.id] = record
257 'type': 'expandable',
258 'domain': [('id','<=', msg.id)]+domain,
260 'thread_level': thread_level # should be improve accodting to level of records
265 # if thread_level > 0:
266 # result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
271 #------------------------------------------------------
273 #------------------------------------------------------
276 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
277 if not cr.fetchone():
278 cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
280 def check(self, cr, uid, ids, mode, context=None):
282 You can read/write a message if:
283 - you received it (a notification exists) or
284 - you can read the related document (res_model, res_id)
285 If a message is not attached to a document, normal access rights on
286 the mail.message object apply.
290 if isinstance(ids, (int, long)):
293 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
295 # check messages for which you have a notification
296 not_obj = self.pool.get('mail.notification')
297 not_ids = not_obj.search(cr, uid, [
298 ('partner_id', '=', partner_id),
299 ('message_id', 'in', ids),
301 for notification in not_obj.browse(cr, uid, not_ids, context=context):
302 if notification.message_id.id in ids:
304 # FP Note: we should put this again !!!
305 #ids.remove(notification.message_id.id)
307 # check messages according to related documents
309 cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
310 for rmod, rid in cr.fetchall():
311 if not (rmod and rid):
313 res_ids.setdefault(rmod,set()).add(rid)
315 ima_obj = self.pool.get('ir.model.access')
316 for model, mids in res_ids.items():
317 mids = self.pool.get(model).exists(cr, uid, mids)
318 ima_obj.check(cr, uid, model, mode)
319 self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
321 def create(self, cr, uid, values, context=None):
322 if not values.get('message_id') and values.get('res_id') and values.get('model'):
323 values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s'% values)
324 newid = super(mail_message, self).create(cr, uid, values, context)
325 self.check(cr, uid, [newid], mode='create', context=context)
326 self.notify(cr, uid, newid, context=context)
329 def notify(self, cr, uid, newid, context=None):
330 """ Add the related record followers to the destination partner_ids.
331 Call mail_notification.notify to manage the email sending
333 followers_obj = self.pool.get("mail.followers")
334 message = self.browse(cr, uid, newid, context=context)
335 partners_to_notify = set([])
336 # add all partner_ids of the message
337 if message.partner_ids:
338 partners_to_notify |= set(partner.id for partner in message.partner_ids)
339 # add all followers and set add them in partner_ids
340 if message.model and message.res_id:
341 record = self.pool.get(message.model).browse(cr, uid, message.res_id, context=context)
342 extra_notified = set(partner.id for partner in record.message_follower_ids)
343 missing_notified = extra_notified - partners_to_notify
344 missing_follow_ids = []
345 if message.subtype_id:
346 for p_id in missing_notified:
347 follow_ids = followers_obj.search(cr, uid, [('partner_id','=',p_id),('subtype_ids','in',[message.subtype_id.id]),('res_model','=',message.model),('res_id','=',message.res_id)])
348 if follow_ids and len(follow_ids):
349 missing_follow_ids.append(p_id)
350 message.write({'partner_ids': [(4, p_id) for p_id in missing_follow_ids]})
351 partners_to_notify |= extra_notified
352 self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
354 def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
355 self.check(cr, uid, ids, 'read', context=context)
356 return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
358 def copy(self, cr, uid, id, default=None, context=None):
359 """Overridden to avoid duplicating fields that are unique to each email"""
362 self.check(cr, uid, [id], 'read', context=context)
363 default.update(message_id=False, headers=False)
364 return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
366 def write(self, cr, uid, ids, vals, context=None):
367 result = super(mail_message, self).write(cr, uid, ids, vals, context)
368 self.check(cr, uid, ids, 'write', context=context)
371 def unlink(self, cr, uid, ids, context=None):
372 self.check(cr, uid, ids, 'unlink', context=context)
373 return super(mail_message, self).unlink(cr, uid, ids, context)
376 class mail_notification(osv.Model):
377 """ mail_notification is a relational table modeling messages pushed to partners.
379 _inherit = 'mail.notification'
381 'message_id': fields.many2one('mail.message', string='Message',
382 ondelete='cascade', required=True, select=1),
385 def set_message_read(self, cr, uid, msg_id, context=None):
386 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
387 notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
388 return self.write(cr, uid, notif_ids, {'read': True}, context=context)