[IMP] mail_thread: avatar -> image_small
[odoo/odoo.git] / addons / mail / mail_thread.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009-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 base64
23 import email
24 from email.utils import parsedate
25 import logging
26 from mail_message import decode, to_email
27 from operator import itemgetter
28 from osv import osv, fields
29 import re
30 import time
31 import tools
32 from tools.translate import _
33 import xmlrpclib
34
35 _logger = logging.getLogger(__name__)
36
37 class mail_thread(osv.osv):
38     '''Mixin model, meant to be inherited by any model that needs to
39        act as a discussion topic on which messages can be attached.
40        Public methods are prefixed with ``message_`` in order to avoid
41        name collisions with methods of the models that will inherit
42        from this mixin.
43
44        ``mail.thread`` is designed to work without adding any field
45        to the extended models. All functionalities and expected behavior
46        are managed by mail.thread, using model name and record ids.
47        A widget has been designed for the 6.1 and following version of OpenERP
48        web-client. However, due to technical limitations, ``mail.thread``
49        adds a simulated one2many field, to display the web widget by
50        overriding the default field displayed. Using this field
51        is not recommanded has it will disappeear in future version
52        of OpenERP, leading to a pure mixin class.
53
54        Inheriting classes are not required to implement any method, as the
55        default implementation will work for any model. However it is common
56        to override at least the ``message_new`` and ``message_update``
57        methods (calling ``super``) to add model-specific behavior at
58        creation and update of a thread; and ``message_get_subscribers``
59        to manage more precisely the social aspect of the thread through
60        the followers.
61     '''
62     _name = 'mail.thread'
63     _description = 'Email Thread'
64
65     def _get_message_ids(self, cr, uid, ids, name, arg, context=None):
66         res = {}
67         for id in ids:
68             res[id] = self.message_load_ids(cr, uid, [id], context=context)
69         return res
70
71     def _search_message_ids(self, cr, uid, obj, name, args, context=None):
72         msg_obj = self.pool.get('mail.message')
73         msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context)
74         return [('id', 'in', msg_ids)]
75
76     # OpenChatter: message_ids is a dummy field that should not be used
77     _columns = {
78         'message_ids': fields.function(_get_message_ids, method=True, fnct_search=_search_message_ids,
79                         type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'),
80     }
81
82     #------------------------------------------------------
83     # Automatic subscription when creating/reading
84     #------------------------------------------------------
85
86     def create(self, cr, uid, vals, context=None):
87         """Automatically subscribe the creator"""
88         thread_id = super(mail_thread, self).create(cr, uid, vals, context=context);
89         self.message_subscribe(cr, uid, [thread_id], [uid], context=context)
90         return thread_id;
91
92     def write(self, cr, uid, ids, vals, context=None):
93         """Automatically subscribe the writer"""
94         if isinstance(ids, (int, long)):
95             ids = [ids]
96         write_res = super(mail_thread, self).write(cr, uid, ids, vals, context=context);
97         if write_res:
98             self.message_subscribe(cr, uid, ids, [uid], context=context)
99         return write_res;
100
101     def unlink(self, cr, uid, ids, context=None):
102         """Override unlink, to automatically delete
103            - subscriptions
104            - messages
105            that are linked with res_model and res_id, not through
106            a foreign key with a 'cascade' ondelete attribute.
107            Notifications will be deleted with messages
108         """
109         if context is None:
110             context = {}
111         subscr_obj = self.pool.get('mail.subscription')
112         msg_obj = self.pool.get('mail.message')
113         # delete subscriptions
114         subscr_to_del_ids = subscr_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
115         subscr_obj.unlink(cr, uid, subscr_to_del_ids, context=context)
116         # delete notifications
117         msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
118         msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
119
120         return super(mail_thread, self).unlink(cr, uid, ids, context=context)
121
122     #------------------------------------------------------
123     # Generic message api
124     #------------------------------------------------------
125
126     def message_create(self, cr, uid, thread_id, vals, context=None):
127         """OpenSocial: wrapper of mail.message create method
128            - creates the mail.message
129            - automatically subscribe the message writer
130            - push the message to subscribed users
131         """
132         if context is None:
133             context = {}
134         message_obj = self.pool.get('mail.message')
135         subscription_obj = self.pool.get('mail.subscription')
136         notification_obj = self.pool.get('mail.notification')
137         res_users_obj = self.pool.get('res.users')
138         body = vals.get('body_html', '') if vals.get('subtype', 'plain') == 'html' else vals.get('body_text', '')
139
140         # automatically subscribe the writer of the message
141         if vals['user_id']:
142             self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
143
144         # get users that will get a notification pushed
145         user_to_push_ids = self.message_create_get_notification_user_ids(cr, uid, [thread_id], vals, context=context)
146         user_to_push_from_parse_ids = self.message_parse_users(cr, uid, [thread_id], body, context=context)
147
148         # set email_from and email_to for comments and notifications
149         if vals.get('type', False) and vals['type'] == 'comment' or vals['type'] == 'notification':
150             current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
151             if not vals.get('email_from', False):
152                 vals['email_from'] = current_user.user_email
153             if not vals.get('email_to', False):
154                 email_to = ''
155                 for user in res_users_obj.browse(cr, uid, user_to_push_ids, context=context):
156                     if not user.notification_email_pref == 'all' and \
157                         not (user.notification_email_pref == 'comments' and vals['type'] == 'comment') and \
158                         not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
159                         continue
160                     if not user.user_email:
161                         continue
162                     email_to = '%s, %s' % (email_to, user.user_email)
163                 email_to = email_to.lstrip(', ')
164                 if email_to:
165                     vals['email_to'] = email_to
166                     vals['state'] = 'outgoing'
167
168         # create message
169         msg_id = message_obj.create(cr, uid, vals, context=context)
170
171         # special: if install mode, do not push demo data
172         if context.get('install_mode', False):
173             return True
174
175         # push to users
176         for id in user_to_push_ids:
177             notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
178
179         return msg_id
180
181     def message_create_get_notification_user_ids(self, cr, uid, thread_ids, new_msg_vals, context=None):
182         if context is None:
183             context = {}
184
185         notif_user_ids = []
186         body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
187         for thread_id in thread_ids:
188             # add subscribers
189             notif_user_ids += [user['id'] for user in self.message_get_subscribers(cr, uid, [thread_id], context=context)]
190             # add users requested via parsing message (@login)
191             notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
192             # add users requested to perform an action (need_action mechanism)
193             if hasattr(self, 'get_needaction_user_ids'):
194                 notif_user_ids += self.get_needaction_user_ids(cr, uid, [thread_id], context=context)[thread_id]
195             # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
196             if new_msg_vals.get('parent_id'):
197                 notif_obj = self.pool.get('mail.notification')
198                 parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
199                 parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
200                 notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
201
202         # remove duplicate entries
203         notif_user_ids = list(set(notif_user_ids))
204         return notif_user_ids
205
206     def message_parse_users(self, cr, uid, ids, string, context=None):
207         """Parse message content
208            - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
209              this supports login that are emails (such as @admin@lapin.net)
210         """
211         regex = re.compile('(^|\s)@((\w|@|\.)*)')
212         login_lst = [item[1] for item in regex.findall(string)]
213         if not login_lst: return []
214         user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context)
215         return user_ids
216
217     def message_capable_models(self, cr, uid, context=None):
218         ret_dict = {}
219         for model_name in self.pool.obj_list():
220             model = self.pool.get(model_name)
221             if 'mail.thread' in getattr(model, '_inherit', []):
222                 ret_dict[model_name] = model._description
223         return ret_dict
224
225     def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
226                         parent_id=False, type='email', subtype='plain', state='received',
227                         email_to=False, email_from=False, email_cc=None, email_bcc=None,
228                         reply_to=None, email_date=None, message_id=False, references=None,
229                         attachments=None, headers=None, original=None, context=None):
230         """Creates a new mail.message attached to the current mail.thread,
231            containing all the details passed as parameters.  All attachments
232            will be attached to the thread record as well as to the actual
233            message.
234            If ``email_from`` is not set or ``type`` not set as 'email',
235            a note message is created, without the usual envelope
236            attributes (sender, recipients, etc.).
237            The creation of the message is done by calling ``message_create``
238            method, that will manage automatic pushing of notifications.
239
240         :param threads: list of thread ids, or list of browse_records representing
241                         threads to which a new message should be attached
242         :param subject: subject of the message, or description of the event if this
243                         is an *event log* entry.
244         :param body_text: plaintext contents of the mail or log message
245         :param body_html: html contents of the mail or log message
246         :param parent_id: id of the parent message (threaded messaging model)
247         :param type: optional type of message: 'email', 'comment', 'notification'
248         :param subtype: optional subtype of message: 'plain' or 'html', corresponding to the main
249                         body contents (body_text or body_html).
250         :param state: optional state of message; 'received' by default
251         :param email_to: Email-To / Recipient address
252         :param email_from: Email From / Sender address if any
253         :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
254         :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
255         :param reply_to: reply_to header
256         :param email_date: email date string if different from now, in server timezone
257         :param message_id: optional email identifier
258         :param references: optional email references
259         :param headers: mail headers to store
260         :param dict attachments: map of attachment filenames to binary contents, if any.
261         :param str original: optional full source of the RFC2822 email, for reference
262         :param dict context: if a ``thread_model`` value is present
263                              in the context, its value will be used
264                              to determine the model of the thread to
265                              update (instead of the current model).
266         """
267         if context is None:
268             context = {}
269         if attachments is None:
270             attachments = {}
271
272         if email_date:
273             edate = parsedate(email_date)
274             if edate is not None:
275                 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
276
277         if all(isinstance(thread_id, (int, long)) for thread_id in threads):
278             model = context.get('thread_model') or self._name
279             model_pool = self.pool.get(model)
280             threads = model_pool.browse(cr, uid, threads, context=context)
281
282         ir_attachment = self.pool.get('ir.attachment')
283         mail_message = self.pool.get('mail.message')
284
285         new_msg_ids = []
286         for thread in threads:
287             to_attach = []
288             for attachment in attachments:
289                 fname, fcontent = attachment
290                 if isinstance(fcontent, unicode):
291                     fcontent = fcontent.encode('utf-8')
292                 data_attach = {
293                     'name': fname,
294                     'datas': base64.b64encode(str(fcontent)),
295                     'datas_fname': fname,
296                     'description': _('Mail attachment'),
297                     'res_model': thread._name,
298                     'res_id': thread.id,
299                 }
300                 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
301
302             partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
303             if not partner_id and thread._name == 'res.partner':
304                 partner_id = thread.id
305             data = {
306                 'subject': subject,
307                 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
308                 'body_html': body_html or '',
309                 'parent_id': parent_id,
310                 'date': email_date or fields.datetime.now(),
311                 'type': type,
312                 'subtype': subtype,
313                 'state': state,
314                 'message_id': message_id,
315                 'attachment_ids': [(6, 0, to_attach)],
316                 'user_id': uid,
317                 'model' : thread._name,
318                 'res_id': thread.id,
319                 'partner_id': partner_id,
320             }
321
322             if email_from or type == 'email':
323                 for param in (email_to, email_cc, email_bcc):
324                     if isinstance(param, list):
325                         param = ", ".join(param)
326                 data.update({
327                     'subject': subject or _('History'),
328                     'email_to': email_to,
329                     'email_from': email_from or \
330                         (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
331                     'email_cc': email_cc,
332                     'email_bcc': email_bcc,
333                     'references': references,
334                     'headers': headers,
335                     'reply_to': reply_to,
336                     'original': original, })
337
338             new_msg_ids.append(self.message_create(cr, uid, thread.id, data, context=context))
339         return new_msg_ids
340
341     def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
342         """Creates a new mail.message attached to the given threads (``ids``),
343            with the contents of ``msg_dict``, by calling ``message_append``
344            with the mail details. All attachments in msg_dict will be
345            attached to the object record as well as to the actual
346            mail message.
347
348            :param dict msg_dict: a map containing the email details and
349                                  attachments. See ``message_process()`` and
350                                 ``mail.message.parse()`` for details on
351                                 the dict structure.
352            :param dict context: if a ``thread_model`` value is present
353                                 in the context, its value will be used
354                                 to determine the model of the thread to
355                                 update (instead of the current model).
356         """
357         return self.message_append(cr, uid, ids,
358                             subject = msg_dict.get('subject'),
359                             body_text = msg_dict.get('body_text'),
360                             body_html= msg_dict.get('body_html'),
361                             parent_id = msg_dict.get('parent_id', False),
362                             type = msg_dict.get('type', 'email'),
363                             subtype = msg_dict.get('subtype', 'plain'),
364                             state = msg_dict.get('state', 'received'),
365                             email_from = msg_dict.get('from', msg_dict.get('email_from')),
366                             email_to = msg_dict.get('to', msg_dict.get('email_to')),
367                             email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
368                             email_bcc = msg_dict.get('bcc', msg_dict.get('email_bcc')),
369                             reply_to = msg_dict.get('reply', msg_dict.get('reply_to')),
370                             email_date = msg_dict.get('date'),
371                             message_id = msg_dict.get('message-id', msg_dict.get('message_id')),
372                             references = msg_dict.get('references')\
373                                       or msg_dict.get('in-reply-to'),
374                             attachments = msg_dict.get('attachments'),
375                             headers = msg_dict.get('headers'),
376                             original = msg_dict.get('original'),
377                             context = context)
378
379     # Message loading
380     def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
381         """ Given message child_ids
382             Find their ancestors until root ids"""
383         if context is None:
384             context = {}
385         msg_obj = self.pool.get('mail.message')
386         tmp_msgs = msg_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
387         parent_ids = [msg['parent_id'][0] for msg in tmp_msgs if msg['parent_id'] and msg['parent_id'][0] not in root_ids and msg['parent_id'][0] not in child_ids]
388         child_ids += parent_ids
389         cur_iter = 0; max_iter = 100; # avoid infinite loop
390         while (parent_ids and (cur_iter < max_iter)):
391             cur_iter += 1
392             tmp_msgs = msg_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
393             parent_ids = [msg['parent_id'][0] for msg in tmp_msgs if msg['parent_id'] and msg['parent_id'][0] not in root_ids and msg['parent_id'][0] not in child_ids]
394             child_ids += parent_ids
395         if (cur_iter > max_iter):
396             _logger.warning("Possible infinite loop in _message_add_ancestor_ids. Note that this algorithm is intended to check for cycle in message graph.")
397         return child_ids
398
399     def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
400         """ OpenChatter feature: return thread messages ids. It searches in
401             mail.messages where res_id = ids, (res_)model = current model.
402             :param domain: domain to add to the search; especially child_of
403                            is interesting when dealing with threaded display
404             :param ascent: performs an ascended search; will add to fetched msgs
405                            all their parents until root_ids
406             :param root_ids: for ascent search
407             :param root_ids: root_ids when performing an ascended search
408         """
409         if context is None:
410             context = {}
411         msg_obj = self.pool.get('mail.message')
412         msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', ids), ('model', '=', self._name)] + domain,
413             limit=limit, offset=offset, context=context)
414         if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
415         return msg_ids
416
417     def message_load(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
418         """ OpenChatter feature: return thread messages
419         """
420         msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
421         msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
422         
423         """ Retrieve all attachments names """
424         map_id_to_name = {}
425         
426         for msg in msgs:
427             for attach_id in msg["attachment_ids"]:
428                 map_id_to_name[attach_id] = '' # use empty string as a placeholder
429         
430         ids = map_id_to_name.keys()
431         names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
432         
433         # convert the list of tuples into a dictionnary
434         for name in names: 
435             map_id_to_name[name[0]] = name[1]
436         
437         # give corresponding ids and names to each message
438         for msg in msgs:
439             msg["attachments"] = []
440             
441             for attach_id in msg["attachment_ids"]:
442                 msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
443         
444         """ Sort and return messages """
445         msgs = sorted(msgs, key=lambda d: (-d['id']))
446         return msgs
447
448     def get_pushed_messages(self, cr, uid, ids, limit=100, offset=0, msg_search_domain=[], ascent=False, root_ids=[], context=None):
449         """ OpenChatter: wall: get messages to display (=pushed notifications)
450             :param domain: domain to add to the search; especially child_of
451                            is interesting when dealing with threaded display
452             :param ascent: performs an ascended search; will add to fetched msgs
453                            all their parents until root_ids
454             :param root_ids: for ascent search
455             :return list of mail.messages sorted by date
456         """
457         if context is None: context = {}
458         notification_obj = self.pool.get('mail.notification')
459         msg_obj = self.pool.get('mail.message')
460         # update message search
461         for arg in msg_search_domain:
462             if isinstance(arg, (tuple, list)):
463                 arg[0] = 'message_id.' + arg[0]
464         # compose final domain
465         domain = [('user_id', '=', uid)] + msg_search_domain
466         # get notifications
467         notification_ids = notification_obj.search(cr, uid, domain, limit=limit, offset=offset, context=context)
468         notifications = notification_obj.browse(cr, uid, notification_ids, context=context)
469         msg_ids = [notification.message_id.id for notification in notifications]
470         # get messages
471         msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
472         if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
473         msgs = msg_obj.read(cr, uid, msg_ids, context=context)
474         return msgs
475
476     #------------------------------------------------------
477     # Email specific
478     #------------------------------------------------------
479     # message_process will call either message_new or message_update.
480
481     def message_process(self, cr, uid, model, message, custom_values=None,
482                         save_original=False, strip_attachments=False,
483                         context=None):
484         """Process an incoming RFC2822 email message related to the
485            given thread model, relying on ``mail.message.parse()``
486            for the parsing operation, and then calling ``message_new``
487            (if the thread record did not exist) or ``message_update``
488            (if it did), then calling ``message_forward`` to automatically
489            notify other people that should receive this message.
490
491            :param string model: the thread model for which a new message
492                                 must be processed
493            :param message: source of the RFC2822 mail
494            :type message: string or xmlrpclib.Binary
495            :type dict custom_values: optional dictionary of field values
496                                     to pass to ``message_new`` if a new
497                                     record needs to be created. Ignored
498                                     if the thread record already exists.
499            :param bool save_original: whether to keep a copy of the original
500                email source attached to the message after it is imported.
501            :param bool strip_attachments: whether to strip all attachments
502                before processing the message, in order to save some space.
503         """
504         # extract message bytes - we are forced to pass the message as binary because
505         # we don't know its encoding until we parse its headers and hence can't
506         # convert it to utf-8 for transport between the mailgate script and here.
507         if isinstance(message, xmlrpclib.Binary):
508             message = str(message.data)
509
510         model_pool = self.pool.get(model)
511         if self._name != model:
512             if context is None: context = {}
513             context.update({'thread_model': model})
514
515         mail_message = self.pool.get('mail.message')
516         res_id = False
517
518         # Parse Message
519         # Warning: message_from_string doesn't always work correctly on unicode,
520         # we must use utf-8 strings here :-(
521         if isinstance(message, unicode):
522             message = message.encode('utf-8')
523         msg_txt = email.message_from_string(message)
524         msg = mail_message.parse_message(msg_txt, save_original=save_original)
525
526         if strip_attachments and 'attachments' in msg:
527             del msg['attachments']
528
529         # Create New Record into particular model
530         def create_record(msg):
531             if hasattr(model_pool, 'message_new'):
532                 return model_pool.message_new(cr, uid, msg,
533                                               custom_values,
534                                               context=context)
535         res_id = False
536         if msg.get('references') or msg.get('in-reply-to'):
537             references = msg.get('references') or msg.get('in-reply-to')
538             if '\r\n' in references:
539                 references = references.split('\r\n')
540             else:
541                 references = references.split(' ')
542             for ref in references:
543                 ref = ref.strip()
544                 res_id = tools.reference_re.search(ref)
545                 if res_id:
546                     res_id = res_id.group(1)
547                 else:
548                     res_id = tools.res_re.search(msg['subject'])
549                     if res_id:
550                         res_id = res_id.group(1)
551                 if res_id:
552                     res_id = int(res_id)
553                     if model_pool.exists(cr, uid, res_id):
554                         if hasattr(model_pool, 'message_update'):
555                             model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
556                     else:
557                         # referenced thread was not found, we'll have to create a new one
558                         res_id = False
559         if not res_id:
560             res_id = create_record(msg)
561         #To forward the email to other followers
562         self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
563         return res_id
564
565     def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
566         """Called by ``message_process`` when a new message is received
567            for a given thread model, if the message did not belong to
568            an existing thread.
569            The default behavior is to create a new record of the corresponding
570            model (based on some very basic info extracted from the message),
571            then attach the message to the newly created record
572            (by calling ``message_append_dict``).
573            Additional behavior may be implemented by overriding this method.
574
575            :param dict msg_dict: a map containing the email details and
576                                  attachments. See ``message_process`` and
577                                 ``mail.message.parse`` for details.
578            :param dict custom_values: optional dictionary of additional
579                                       field values to pass to create()
580                                       when creating the new thread record.
581                                       Be careful, these values may override
582                                       any other values coming from the message.
583            :param dict context: if a ``thread_model`` value is present
584                                 in the context, its value will be used
585                                 to determine the model of the record
586                                 to create (instead of the current model).
587            :rtype: int
588            :return: the id of the newly created thread object
589         """
590         if context is None:
591             context = {}
592         model = context.get('thread_model') or self._name
593         model_pool = self.pool.get(model)
594         fields = model_pool.fields_get(cr, uid, context=context)
595         data = model_pool.default_get(cr, uid, fields, context=context)
596         if 'name' in fields and not data.get('name'):
597             data['name'] = msg_dict.get('from','')
598         if custom_values and isinstance(custom_values, dict):
599             data.update(custom_values)
600         res_id = model_pool.create(cr, uid, data, context=context)
601         self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
602         return res_id
603
604     def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
605         """Called by ``message_process`` when a new message is received
606            for an existing thread. The default behavior is to create a
607            new mail.message in the given thread (by calling
608            ``message_append_dict``)
609            Additional behavior may be implemented by overriding this
610            method.
611
612            :param dict msg_dict: a map containing the email details and
613                                 attachments. See ``message_process`` and
614                                 ``mail.message.parse()`` for details.
615            :param dict context: if a ``thread_model`` value is present
616                                 in the context, its value will be used
617                                 to determine the model of the thread to
618                                 update (instead of the current model).
619         """
620         return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
621
622     def message_thread_followers(self, cr, uid, ids, context=None):
623         """Returns a list of email addresses of the people following
624            this thread, including the sender of each mail, and the
625            people who were in CC of the messages, if any.
626         """
627         res = {}
628         if isinstance(ids, (str, int, long)):
629             ids = [long(ids)]
630         for thread in self.browse(cr, uid, ids, context=context):
631             l = set()
632             for message in thread.message_ids:
633                 l.add((message.user_id and message.user_id.user_email) or '')
634                 l.add(message.email_from or '')
635                 l.add(message.email_cc or '')
636             res[thread.id] = filter(None, l)
637         return res
638
639     def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
640         """Sends an email to all people following the given threads.
641            The emails are forwarded immediately, not queued for sending,
642            and not archived.
643
644         :param str model: thread model
645         :param list thread_ids: ids of the thread records
646         :param msg: email.message.Message object to forward
647         :param email_error: optional email address to notify in case
648                             of any delivery error during the forward.
649         :return: True
650         """
651         model_pool = self.pool.get(model)
652         smtp_server_obj = self.pool.get('ir.mail_server')
653         mail_message = self.pool.get('mail.message')
654         for res in model_pool.browse(cr, uid, thread_ids, context=context):
655             if hasattr(model_pool, 'message_thread_followers'):
656                 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
657             else:
658                 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
659             message_followers_emails = to_email(','.join(filter(None, followers)))
660             message_recipients = to_email(','.join(filter(None,
661                                                                        [decode(msg['from']),
662                                                                         decode(msg['to']),
663                                                                         decode(msg['cc'])])))
664             forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
665             if forward_to:
666                 # TODO: we need an interface for this for all types of objects, not just leads
667                 if hasattr(res, 'section_id'):
668                     del msg['reply-to']
669                     msg['reply-to'] = res.section_id.reply_to
670
671                 smtp_from, = to_email(msg['from'])
672                 msg['from'] = smtp_from
673                 msg['to'] =  ", ".join(forward_to)
674                 msg['message-id'] = tools.generate_tracking_message_id(res.id)
675                 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
676                     subj = msg['subject']
677                     del msg['subject'], msg['to'], msg['cc'], msg['bcc']
678                     msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
679                     msg['to'] = email_error
680                     smtp_server_obj.send_email(cr, uid, msg)
681         return True
682
683     def message_partner_by_email(self, cr, uid, email, context=None):
684         """Attempts to return the id of a partner address matching
685            the given ``email``, and the corresponding partner id.
686            Can be used by classes using the ``mail.thread`` mixin
687            to lookup the partner and use it in their implementation
688            of ``message_new`` to link the new record with a
689            corresponding partner.
690            The keys used in the returned dict are meant to map
691            to usual names for relationships towards a partner
692            and one of its addresses.
693
694            :param email: email address for which a partner
695                          should be searched for.
696            :rtype: dict
697            :return: a map of the following form::
698
699                       { 'partner_address_id': id or False,
700                         'partner_id': pid or False }
701         """
702         partner_pool = self.pool.get('res.partner')
703         res = {'partner_id': False}
704         if email:
705             email = to_email(email)[0]
706             contact_ids = partner_pool.search(cr, uid, [('email', '=', email)])
707             if contact_ids:
708                 contact = partner_pool.browse(cr, uid, contact_ids[0])
709                 res['partner_id'] = contact.id
710         return res
711
712     # for backwards-compatibility with old scripts
713     process_email = message_process
714
715     #------------------------------------------------------
716     # Note specific
717     #------------------------------------------------------
718
719     def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
720         if context is None:
721             context = {}
722         notification_obj = self.pool.get('mail.notification')
723         # write message
724         msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, subtype=subtype, context=context)
725         # escape if in install mode or note writing was not successfull
726         if 'install_mode' in context:
727             return True
728         if not isinstance(msg_ids, (list)):
729             return True
730         # get already existing notigications
731         notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
732         already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
733         # get base.group_user group
734         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
735         group_id = res and res[1] or False
736         if not group_id: return True
737         group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
738         for user in group.users:
739             if user.id in already_pushed_user_ids: continue
740             for msg_id in msg_ids:
741                 notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
742         return True
743
744     def log(self, cr, uid, id, message, secondary=False, context=None):
745         _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
746         self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
747
748     def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
749         if subject is None:
750             if type == 'notification':
751                 subject = _('System notification')
752             elif type == 'comment' and not parent_id:
753                 subject = _('Comment')
754             elif type == 'comment' and parent_id:
755                 subject = _('Reply')
756         if subtype == 'html':
757             body_html = body
758             body_text = body
759         else:
760             body_html = body
761             body_text = body
762         return self.message_append(cr, uid, ids, subject, body_html=body_html, body_text=body_text, parent_id=parent_id, type=type, subtype=subtype, context=context)
763
764     #------------------------------------------------------
765     # Subscription mechanism
766     #------------------------------------------------------
767
768     def message_get_subscribers_ids(self, cr, uid, ids, context=None):
769         subscr_obj = self.pool.get('mail.subscription')
770         subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
771         subs = subscr_obj.read(cr, uid, subscr_ids, context=context)
772         return [sub['user_id'][0] for sub in subs]
773
774     def message_get_subscribers(self, cr, uid, ids, context=None):
775         user_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context)
776         users = self.pool.get('res.users').read(cr, uid, user_ids, fields=['id', 'name', 'image_small'], context=context)
777         return users
778
779     def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None):
780         users = self.message_get_subscribers(cr, uid, ids, context=context)
781         sub_user_id = uid if user_id is None else user_id
782         if sub_user_id in [user['id'] for user in users]:
783             return True
784         return False
785
786     def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
787         subscription_obj = self.pool.get('mail.subscription')
788         to_subscribe_uids = [uid] if user_ids is None else user_ids
789         create_ids = []
790         for id in ids:
791             for user_id in to_subscribe_uids:
792                 if self.message_is_subscriber(cr, uid, [id], user_id=user_id, context=context): continue
793                 create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context))
794         return create_ids
795
796     def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
797         if not user_ids and not uid in self.message_get_subscribers_ids(cr, uid, ids, context=context):
798             return False
799         subscription_obj = self.pool.get('mail.subscription')
800         to_unsubscribe_uids = [uid] if user_ids is None else user_ids
801         to_delete_sub_ids = subscription_obj.search(cr, uid,
802                         ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
803         subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
804         return True
805
806     #------------------------------------------------------
807     # Notification API
808     #------------------------------------------------------
809
810     def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
811         if context is None:
812             context = {}
813         notif_obj = self.pool.get('mail.notification')
814         msg_obj = self.pool.get('mail.message')
815         if remove_childs:
816             notif_msg_ids = msg_obj.search(cr, uid, [('id', 'child_of', msg_ids)], context=context)
817         else:
818             notif_msg_ids = msg_ids
819         to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context)
820         return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context)
821
822 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: