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 """ Messages model: system notification (replacing res.log notifications),
38 comments (OpenChatter discussion) and incoming emails. """
39 _name = 'mail.message'
40 _description = 'Message'
41 _inherit = ['ir.needaction_mixin']
44 _message_read_limit = 10
45 _message_record_name_length = 18
47 def _shorten_name(self, name):
48 if len(name) <= (self._message_record_name_length+3):
50 return name[:self._message_record_name_length] + '...'
52 def _get_record_name(self, cr, uid, ids, name, arg, context=None):
53 """ Return the related document name, using get_name. """
54 result = dict.fromkeys(ids, '')
55 for message in self.browse(cr, uid, ids, context=context):
56 if not message.model or not message.res_id:
58 result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
61 def _get_unread(self, cr, uid, ids, name, arg, context=None):
62 """ Compute if the message is unread by the current user. """
63 res = dict((id, {'unread': False}) for id in ids)
64 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
65 notif_obj = self.pool.get('mail.notification')
66 notif_ids = notif_obj.search(cr, uid, [
67 ('partner_id', 'in', [partner_id]),
68 ('message_id', 'in', ids),
71 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
72 res[notif.message_id.id]['unread'] = True
75 def _search_unread(self, cr, uid, obj, name, domain, context=None):
76 """ Search for messages unread by the current user. Condition is
77 inversed because we search unread message on a read column. """
79 read_cond = '(read = false or read is null)'
81 read_cond = 'read = true'
82 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
83 cr.execute("SELECT message_id FROM mail_notification "\
84 "WHERE partner_id = %%s AND %s" % read_cond,
86 return [('id', 'in', [r[0] for r in cr.fetchall()])]
88 def name_get(self, cr, uid, ids, context=None):
89 # name_get may receive int id instead of an id list
90 if isinstance(ids, (int, long)):
93 for message in self.browse(cr, uid, ids, context=context):
94 name = '%s: %s' % (message.subject or '', message.body or '')
95 res.append((message.id, self._shorten_name(name.lstrip(' :'))))
99 'type': fields.selection([
101 ('comment', 'Comment'),
102 ('notification', 'System notification'),
104 help="Message type: email for email message, notification for system "\
105 "message, comment for other messages such as user replies"),
106 'author_id': fields.many2one('res.partner', 'Author', required=True),
107 'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
108 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
109 'message_id', 'attachment_id', 'Attachments'),
110 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
111 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
112 'model': fields.char('Related Document Model', size=128, select=1),
113 'res_id': fields.integer('Related Document ID', select=1),
114 'record_name': fields.function(_get_record_name, type='string',
115 string='Message Record Name',
116 help="Name get of the related document."),
117 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
118 'subject': fields.char('Subject'),
119 'date': fields.datetime('Date'),
120 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
121 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
122 'unread': fields.function(_get_unread, fnct_search=_search_unread,
123 type='boolean', string='Unread',
124 help='Functional field to search for unread messages linked to uid'),
127 def _needaction_domain_get(self, cr, uid, context=None):
129 return [('unread', '=', True)]
132 def _get_default_author(self, cr, uid, context=None):
133 return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
137 'date': lambda *a: fields.datetime.now(),
138 'author_id': _get_default_author
141 #------------------------------------------------------
142 # Message loading for web interface
143 #------------------------------------------------------
145 def _message_dict_get(self, cr, uid, msg, context=None):
146 """ Return a dict representation of the message browse record. """
147 attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
148 author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
149 author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
150 partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
154 'attachment_ids': attachment_ids,
157 'res_id': msg.res_id,
158 'record_name': msg.record_name,
159 'subject': msg.subject,
161 'author_id': author_id,
162 'author_user_id': author_user_id,
163 'partner_ids': partner_ids,
167 def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
168 """ Given a tree with several roots of following structure :
170 {'id': 1, 'child_ids':[
171 {'id': 11, 'child_ids': [...] },
175 Flatten it to have a maximum number of level, with 0 being
177 Perform the flattening at leafs if above the maximum depth, then get
180 def _flatten(msg_dict):
181 """ from {'id': x, 'child_ids': [{child1}, {child2}]}
182 get [{'id': x, 'child_ids': []}, {child1}, {child2}]
184 child_ids = msg_dict.pop('child_ids', [])
185 msg_dict['child_ids'] = []
186 return [msg_dict] + child_ids
187 # Depth-first flattening
188 for message in messages:
189 message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level+1, level, context=context)
190 # Flatten if above maximum depth
191 if current_level < level:
194 for x in range(0, len(messages)):
195 flatenned = _flatten(messages[x])
196 for flat in flatenned:
197 new_list.append(flat)
201 def _debug_print_tree(self, tree, prefix=''):
203 print '%s%s (%s childs: %s)' % (prefix, elem['id'], len(elem['child_ids']), [xelem['id'] for xelem in elem['child_ids']])
204 if elem['child_ids']:
205 self._debug_print_tree(elem['child_ids'], prefix+'--')
207 def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
209 If IDS are provided, fetch these records, otherwise use the domain to
210 fetch the matching records. After having fetched the records provided
211 by IDS, it will fetch children (according to thread_level).
217 limit = limit or self._message_read_limit
218 context = context or {}
220 ids = self.search(cr, uid, domain, context=context, limit=limit)
222 # FP Todo: flatten to max X level of mail_thread
223 messages = self.browse(cr, uid, ids, context=context)
226 tree = {} # key: ID, value: record
228 if len(result)<(limit-1):
229 record = self._message_dict_get(cr, uid, msg, context=context)
230 if thread_level and msg.parent_id:
232 if msg.parent_id.id in tree:
233 record_parent = tree[msg.parent_id.id]
235 record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
236 if msg.parent_id.parent_id:
237 tree[msg.parent_id.id] = record_parent
238 if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
239 record_parent['child_ids'].append(record)
240 record = record_parent
242 if msg.id not in tree:
243 result.append(record)
244 tree[msg.id] = record
247 'type': 'expandable',
248 'domain': [('id','<=', msg.id)]+domain,
250 'thread_level': thread_level # should be improve accodting to level of records
255 # if thread_level > 0:
256 # result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
260 #------------------------------------------------------
262 #------------------------------------------------------
265 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
266 if not cr.fetchone():
267 cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
269 def check_access_rule(self, cr, uid, ids, operation, context=None):
270 """ mail.message access rule check
271 - message received (a notification exists) -> ok
272 - check rules of related document if exists
273 - fallback on normal mail.message check """
274 if isinstance(ids, (int, long)):
277 # check messages for which you have a notification
278 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
279 not_obj = self.pool.get('mail.notification')
280 not_ids = not_obj.search(cr, uid, [
281 ('partner_id', '=', partner_id),
282 ('message_id', 'in', ids),
284 notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, uid, not_ids, context=context)
285 if notification.message_id.id in ids]
287 # check messages linked to an existing document
288 model_record_ids = {}
290 cr.execute('SELECT DISTINCT id, model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
291 for id, rmod, rid in cr.fetchall():
292 if not (rmod and rid):
294 document_ids.append(id)
295 model_record_ids.setdefault(rmod,set()).add(rid)
296 for model, mids in model_record_ids.items():
297 model_obj = self.pool.get(model)
298 mids = model_obj.exists(cr, uid, mids)
299 model_obj.check_access_rights(cr, uid, operation)
300 model_obj.check_access_rule(cr, uid, mids, operation, context=context)
302 # fall back on classic operation for other ids
303 other_ids = set(ids).difference(set(notified_ids), set(document_ids))
304 super(mail_message, self).check_access_rule(cr, uid, other_ids, operation, context=None)
306 def create(self, cr, uid, values, context=None):
307 if not values.get('message_id') and values.get('res_id') and values.get('model'):
308 values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s'% values)
309 newid = super(mail_message, self).create(cr, uid, values, context)
310 self.notify(cr, uid, newid, context=context)
313 def notify(self, cr, uid, newid, context=None):
314 """ Add the related record followers to the destination partner_ids.
315 Call mail_notification.notify to manage the email sending
317 message = self.browse(cr, uid, newid, context=context)
318 partners_to_notify = set([])
319 # add all partner_ids of the message
320 if message.partner_ids:
321 partners_to_notify |= set(partner.id for partner in message.partner_ids)
322 # add all followers and set add them in partner_ids
323 if message.model and message.res_id:
324 record = self.pool.get(message.model).browse(cr, uid, message.res_id, context=context)
325 extra_notified = set(partner.id for partner in record.message_follower_ids)
326 missing_notified = extra_notified - partners_to_notify
328 message.write({'partner_ids': [(4, p_id) for p_id in missing_notified]})
329 partners_to_notify |= extra_notified
330 self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
332 def copy(self, cr, uid, id, default=None, context=None):
333 """Overridden to avoid duplicating fields that are unique to each email"""
336 default.update(message_id=False, headers=False)
337 return super(mail_message,self).copy(cr, uid, id, default=default, context=context)