1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2009-today OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
31 from email.message import Message
32 from mail_message import decode
33 from openerp import SUPERUSER_ID
34 from osv import osv, fields
35 from tools.safe_eval import safe_eval as eval
37 _logger = logging.getLogger(__name__)
39 def decode_header(message, header, separator=' '):
40 return separator.join(map(decode, message.get_all(header, [])))
42 class many2many_reference(fields.many2many):
43 """ many2many_reference manages many2many fields where one id is found
44 by a reference-like key (a char column in addition to the foreign id).
45 The reference_column attribute on the many2many fields is used;
46 if not defined, ``res_model`` is used. """
48 def _get_query_and_where_params(self, cr, model, ids, values, where_params):
49 """ Add in where condition like mail_followers.res_model = 'crm.lead' """
50 reference_column = self.reference_column if self.reference_column else 'res_model'
51 values.update(reference_column=reference_column, reference_value=model._name)
52 query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
53 FROM %(rel)s, %(from_c)s \
54 WHERE %(rel)s.%(id1)s IN %%s \
55 AND %(rel)s.%(id2)s = %(tbl)s.id \
56 AND %(rel)s.%(reference_column)s = \'%(reference_value)s\' \
62 return query, where_params
64 def set(self, cr, model, id, name, values, user=None, context=None):
65 """ Override to add the reference field in queries. """
67 rel, id1, id2 = self._sql_names(model)
68 obj = model.pool.get(self._obj)
69 # reference column name: given by attribute or res_model
70 reference_column = self.reference_column if self.reference_column else 'res_model'
72 if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
75 idnew = obj.create(cr, user, act[2], context=context)
76 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, idnew, model._name))
78 cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name))
80 # following queries are in the same transaction - so should be relatively safe
81 cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND '+reference_column+'=%s', (id, act[1], model._name))
83 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act[1], model._name))
85 cr.execute('delete from '+rel+' where '+id1+' = %s AND '+reference_column+'=%s', (id, model._name))
87 d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
89 d1 = ' and ' + ' and '.join(d1)
92 cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+reference_column+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2)
93 for act_nbr in act[2]:
94 cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+','+reference_column+') VALUES (%s,%s,%s)', (id, act_nbr, model._name))
95 # cases 1, 2: performs write and unlink -> default implementation is ok
97 return super(many2many_reference, self).set(cr, model, id, name, values, user, context)
100 class mail_thread(osv.AbstractModel):
101 ''' mail_thread model is meant to be inherited by any model that needs to
102 act as a discussion topic on which messages can be attached. Public
103 methods are prefixed with ``message_`` in order to avoid name
104 collisions with methods of the models that will inherit from this class.
106 ``mail.thread`` defines fields used to handle and display the
107 communication history. ``mail.thread`` also manages followers of
108 inheriting classes. All features and expected behavior are managed
109 by mail.thread. Widgets has been designed for the 7.0 and following
112 Inheriting classes are not required to implement any method, as the
113 default implementation will work for any model. However it is common
114 to override at least the ``message_new`` and ``message_update``
115 methods (calling ``super``) to add model-specific behavior at
116 creation and update of a thread when processing incoming emails.
118 _name = 'mail.thread'
119 _description = 'Email Thread'
120 _mail_autothread = True
122 def _get_message_data(self, cr, uid, ids, name, args, context=None):
124 - message_unread: has uid unread message for the document
125 - message_summary: html snippet summarizing the Chatter for kanban views """
126 res = dict((id, dict(message_unread=False, message_summary='')) for id in ids)
128 # search for unread messages, by reading directly mail.notification, as SUPERUSER
129 notif_obj = self.pool.get('mail.notification')
130 notif_ids = notif_obj.search(cr, SUPERUSER_ID, [
131 ('partner_id.user_ids', 'in', [uid]),
132 ('message_id.res_id', 'in', ids),
133 ('message_id.model', '=', self._name),
136 for notif in notif_obj.browse(cr, SUPERUSER_ID, notif_ids, context=context):
137 res[notif.message_id.res_id]['message_unread'] = True
139 for thread in self.browse(cr, uid, ids, context=context):
140 cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or ''
141 res[thread.id]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread.message_comment_ids), len(thread.message_follower_ids))
145 def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
147 - message_is_follower: is uid in the document followers
148 - message_subtype_data: data about document subtypes: which are
149 available, which are followed if any """
150 res = dict((id, dict(message_subtype_data='', message_is_follower=False)) for id in ids)
151 user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
153 # find current model subtypes, add them to a dictionary
154 subtype_obj = self.pool.get('mail.message.subtype')
155 subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
156 subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False, id=subtype.id)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
158 res[id]['message_subtype_data'] = subtype_dict.copy()
160 # find the document followers, update the data
161 fol_obj = self.pool.get('mail.followers')
162 fol_ids = fol_obj.search(cr, uid, [
163 ('partner_id', '=', user_pid),
164 ('res_id', 'in', ids),
165 ('res_model', '=', self._name),
167 for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
168 thread_subtype_dict = res[fol.res_id]['message_subtype_data']
169 res[fol.res_id]['message_is_follower'] = True
170 for subtype in fol.subtype_ids:
171 thread_subtype_dict[subtype.name]['followed'] = True
172 res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
176 def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None):
177 partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
179 notif_obj = self.pool.get('mail.notification')
180 notif_ids = notif_obj.search(cr, uid, [
181 ('partner_id', '=', partner_id),
182 ('message_id.model', '=', self._name),
185 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
186 res[notif.message_id.res_id] = True
187 return [('id', 'in', res.keys())]
190 'message_is_follower': fields.function(_get_subscription_data,
191 type='boolean', string='Is a Follower', multi='_get_subscription_data,'),
192 'message_subtype_data': fields.function(_get_subscription_data,
193 type='text', string='Subscription data', multi="_get_subscription_data",
194 help="Holds data about the subtypes. The content of this field "\
195 "is a structure holding the current model subtypes, and the "\
196 "current document followed subtypes."),
197 'message_follower_ids': many2many_reference('res.partner',
198 'mail_followers', 'res_id', 'partner_id',
199 reference_column='res_model', string='Followers'),
200 'message_comment_ids': fields.one2many('mail.message', 'res_id',
201 domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))],
202 string='Comments and emails',
203 help="Comments and emails"),
204 'message_ids': fields.one2many('mail.message', 'res_id',
205 domain=lambda self: [('model', '=', self._name)],
207 help="Messages and communication history"),
208 'message_unread': fields.function(_get_message_data, fnct_search=_search_unread,
209 type='boolean', string='Unread Messages', multi="_get_message_data",
210 help="If checked new messages require your attention."),
211 'message_summary': fields.function(_get_message_data, method=True,
212 type='text', string='Summary', multi="_get_message_data",
213 help="Holds the Chatter summary (number of messages, ...). "\
214 "This summary is directly in html format in order to "\
215 "be inserted in kanban views."),
218 #------------------------------------------------------
219 # Automatic subscription when creating
220 #------------------------------------------------------
222 def create(self, cr, uid, vals, context=None):
223 """ Override to subscribe the current user. """
224 thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
225 self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
228 def unlink(self, cr, uid, ids, context=None):
229 """ Override unlink to delete messages and followers. This cannot be
230 cascaded, because link is done through (res_model, res_id). """
231 msg_obj = self.pool.get('mail.message')
232 fol_obj = self.pool.get('mail.followers')
233 # delete messages and notifications
234 msg_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
235 msg_obj.unlink(cr, uid, msg_ids, context=context)
237 fol_ids = fol_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
238 fol_obj.unlink(cr, uid, fol_ids, context=context)
239 return super(mail_thread, self).unlink(cr, uid, ids, context=context)
241 def copy(self, cr, uid, id, default=None, context=None):
242 default = default or {}
243 default['message_ids'] = []
244 default['message_comment_ids'] = []
245 default['message_follower_ids'] = []
246 return super(mail_thread, self).copy(cr, uid, id, default=default, context=context)
248 #------------------------------------------------------
249 # mail.message wrappers and tools
250 #------------------------------------------------------
252 def _needaction_domain_get(self, cr, uid, context=None):
254 return [('message_unread', '=', True)]
257 #------------------------------------------------------
259 #------------------------------------------------------
261 def message_capable_models(self, cr, uid, context=None):
262 """ Used by the plugin addon, based for plugin_outlook and others. """
264 for model_name in self.pool.obj_list():
265 model = self.pool.get(model_name)
266 if 'mail.thread' in getattr(model, '_inherit', []):
267 ret_dict[model_name] = model._description
270 def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None):
271 """ Find partners related to some header fields of the message. """
272 s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
273 return [partner_id for email in tools.email_split(s)
274 for partner_id in self.pool.get('res.partner').search(cr, uid, [('email', 'ilike', email)], context=context)]
276 def _message_find_user_id(self, cr, uid, message, context=None):
277 from_local_part = tools.email_split(decode(message.get('From')))[0]
278 # FP Note: canonification required, the minimu: .lower()
279 user_ids = self.pool.get('res.users').search(cr, uid, ['|',
280 ('login', '=', from_local_part),
281 ('email', '=', from_local_part)], context=context)
282 return user_ids[0] if user_ids else uid
284 def message_route(self, cr, uid, message, model=None, thread_id=None,
285 custom_values=None, context=None):
286 """Attempt to figure out the correct target model, thread_id,
287 custom_values and user_id to use for an incoming message.
288 Multiple values may be returned, if a message had multiple
289 recipients matching existing mail.aliases, for example.
291 The following heuristics are used, in this order:
292 1. If the message replies to an existing thread_id, and
293 properly contains the thread model in the 'In-Reply-To'
294 header, use this model/thread_id pair, and ignore
295 custom_value (not needed as no creation will take place)
296 2. Look for a mail.alias entry matching the message
297 recipient, and use the corresponding model, thread_id,
298 custom_values and user_id.
299 3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
301 4. If all the above fails, raise an exception.
303 :param string message: an email.message instance
304 :param string model: the fallback model to use if the message
305 does not match any of the currently configured mail aliases
306 (may be None if a matching alias is supposed to be present)
307 :type dict custom_values: optional dictionary of default field values
308 to pass to ``message_new`` if a new record needs to be created.
309 Ignored if the thread record already exists, and also if a
310 matching mail.alias was found (aliases define their own defaults)
311 :param int thread_id: optional ID of the record/thread from ``model``
312 to which this mail should be attached. Only used if the message
313 does not reply to an existing thread and does not match any mail alias.
314 :return: list of [model, thread_id, custom_values, user_id]
316 assert isinstance(message, Message), 'message must be an email.message.Message at this point'
317 message_id = message.get('Message-Id')
319 # 1. Verify if this is a reply to an existing thread
320 references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To')
321 ref_match = references and tools.reference_re.search(references)
323 thread_id = int(ref_match.group(1))
324 model = ref_match.group(2) or model
325 model_pool = self.pool.get(model)
326 if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
327 and hasattr(model_pool, 'message_update'):
328 _logger.debug('Routing mail with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
329 message_id, model, thread_id, custom_values, uid)
330 return [(model, thread_id, custom_values, uid)]
332 # 2. Look for a matching mail.alias entry
333 # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
334 # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
335 rcpt_tos = decode_header(message, 'Delivered-To') or \
336 ','.join([decode_header(message, 'To'),
337 decode_header(message, 'Cc'),
338 decode_header(message, 'Resent-To'),
339 decode_header(message, 'Resent-Cc')])
340 local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)]
342 mail_alias = self.pool.get('mail.alias')
343 alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
346 for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
347 user_id = alias.alias_user_id.id
349 user_id = self._message_find_user_id(cr, uid, message, context=context)
350 routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
351 eval(alias.alias_defaults), user_id))
352 _logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
355 # 3. Fallback to the provided parameters, if they work
356 model_pool = self.pool.get(model)
358 # Legacy: fallback to matching [ID] in the Subject
359 match = tools.res_re.search(decode_header(message, 'Subject'))
360 thread_id = match and match.group(1)
361 assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
362 "No possible route found for incoming message with Message-Id %s. " \
363 "Create an appropriate mail.alias or force the destination model." % message_id
364 if thread_id and not model_pool.exists(cr, uid, thread_id):
365 _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
366 thread_id, message_id)
368 _logger.debug('Routing mail with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
369 message_id, model, thread_id, custom_values, uid)
370 return [(model, thread_id, custom_values, uid)]
372 def message_process(self, cr, uid, model, message, custom_values=None,
373 save_original=False, strip_attachments=False,
374 thread_id=None, context=None):
375 """Process an incoming RFC2822 email message, relying on
376 ``mail.message.parse()`` for the parsing operation,
377 and ``message_route()`` to figure out the target model.
379 Once the target model is known, its ``message_new`` method
380 is called with the new message (if the thread record did not exist)
381 or its ``message_update`` method (if it did).
383 :param string model: the fallback model to use if the message
384 does not match any of the currently configured mail aliases
385 (may be None if a matching alias is supposed to be present)
386 :param message: source of the RFC2822 message
387 :type message: string or xmlrpclib.Binary
388 :type dict custom_values: optional dictionary of field values
389 to pass to ``message_new`` if a new record needs to be created.
390 Ignored if the thread record already exists, and also if a
391 matching mail.alias was found (aliases define their own defaults)
392 :param bool save_original: whether to keep a copy of the original
393 email source attached to the message after it is imported.
394 :param bool strip_attachments: whether to strip all attachments
395 before processing the message, in order to save some space.
396 :param int thread_id: optional ID of the record/thread from ``model``
397 to which this mail should be attached. When provided, this
398 overrides the automatic detection based on the message
401 if context is None: context = {}
403 # extract message bytes - we are forced to pass the message as binary because
404 # we don't know its encoding until we parse its headers and hence can't
405 # convert it to utf-8 for transport between the mailgate script and here.
406 if isinstance(message, xmlrpclib.Binary):
407 message = str(message.data)
408 # Warning: message_from_string doesn't always work correctly on unicode,
409 # we must use utf-8 strings here :-(
410 if isinstance(message, unicode):
411 message = message.encode('utf-8')
412 msg_txt = email.message_from_string(message)
413 routes = self.message_route(cr, uid, msg_txt, model,
414 thread_id, custom_values,
416 msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
417 if strip_attachments: msg.pop('attachments', None)
419 for model, thread_id, custom_values, user_id in routes:
420 if self._name != model:
421 context.update({'thread_model': model})
422 model_pool = self.pool.get(model)
423 assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
424 "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
425 (msg['message-id'], model)
426 if thread_id and hasattr(model_pool, 'message_update'):
427 model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
429 thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
430 self.message_post(cr, uid, [thread_id], context=context, **msg)
433 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
434 """Called by ``message_process`` when a new message is received
435 for a given thread model, if the message did not belong to
437 The default behavior is to create a new record of the corresponding
438 model (based on some very basic info extracted from the message).
439 Additional behavior may be implemented by overriding this method.
441 :param dict msg_dict: a map containing the email details and
442 attachments. See ``message_process`` and
443 ``mail.message.parse`` for details.
444 :param dict custom_values: optional dictionary of additional
445 field values to pass to create()
446 when creating the new thread record.
447 Be careful, these values may override
448 any other values coming from the message.
449 :param dict context: if a ``thread_model`` value is present
450 in the context, its value will be used
451 to determine the model of the record
452 to create (instead of the current model).
454 :return: the id of the newly created thread object
458 model = context.get('thread_model') or self._name
459 model_pool = self.pool.get(model)
460 fields = model_pool.fields_get(cr, uid, context=context)
461 data = model_pool.default_get(cr, uid, fields, context=context)
462 if 'name' in fields and not data.get('name'):
463 data['name'] = msg_dict.get('subject', '')
464 if custom_values and isinstance(custom_values, dict):
465 data.update(custom_values)
466 res_id = model_pool.create(cr, uid, data, context=context)
469 def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
470 """Called by ``message_process`` when a new message is received
471 for an existing thread. The default behavior is to update the record
472 with update_vals taken from the incoming email.
473 Additional behavior may be implemented by overriding this
475 :param dict msg_dict: a map containing the email details and
476 attachments. See ``message_process`` and
477 ``mail.message.parse()`` for details.
478 :param dict update_vals: a dict containing values to update records
479 given their ids; if the dict is None or is
480 void, no write operation is performed.
483 self.write(cr, uid, ids, update_vals, context=context)
486 def _message_extract_payload(self, message, save_original=False):
487 """Extract body as HTML and attachments from the mail message"""
491 attachments.append(('original_email.eml', message.as_string()))
492 if not message.is_multipart() or 'text/' in message.get('content-type', ''):
493 encoding = message.get_content_charset()
494 body = message.get_payload(decode=True)
495 body = tools.ustr(body, encoding, errors='replace')
496 if message.get_content_type() == 'text/plain':
497 # text/plain -> <pre/>
498 body = tools.append_content_to_html(u'', body)
500 alternative = (message.get_content_type() == 'multipart/alternative')
501 for part in message.walk():
502 if part.get_content_maintype() == 'multipart':
503 continue # skip container
504 filename = part.get_filename() # None if normal part
505 encoding = part.get_content_charset() # None if attachment
506 # 1) Explicit Attachments -> attachments
507 if filename or part.get('content-disposition', '').strip().startswith('attachment'):
508 attachments.append((filename or 'attachment', part.get_payload(decode=True)))
510 # 2) text/plain -> <pre/>
511 if part.get_content_type() == 'text/plain' and (not alternative or not body):
512 body = tools.append_content_to_html(body, tools.ustr(part.get_payload(decode=True),
513 encoding, errors='replace'))
514 # 3) text/html -> raw
515 elif part.get_content_type() == 'text/html':
516 html = tools.ustr(part.get_payload(decode=True), encoding, errors='replace')
520 body = tools.append_content_to_html(body, html, plaintext=False)
521 # 4) Anything else -> attachment
523 attachments.append((filename or 'attachment', part.get_payload(decode=True)))
524 return body, attachments
526 def message_parse(self, cr, uid, message, save_original=False, context=None):
527 """Parses a string or email.message.Message representing an
528 RFC-2822 email, and returns a generic dict holding the
531 :param message: the message to parse
532 :type message: email.message.Message | string | unicode
533 :param bool save_original: whether the returned dict
534 should include an ``original`` attachment containing
535 the source of the message
537 :return: A dict with the following structure, where each
538 field may not be present if missing in original
541 { 'message-id': msg_id,
546 'body': unified_body,
547 'attachments': [('file1', 'bytes'),
552 if not isinstance(message, Message):
553 if isinstance(message, unicode):
554 # Warning: message_from_string doesn't always work correctly on unicode,
555 # we must use utf-8 strings here :-(
556 message = message.encode('utf-8')
557 message = email.message_from_string(message)
559 message_id = message['message-id']
561 # Very unusual situation, be we should be fault-tolerant here
562 message_id = "<%s@localhost>" % time.time()
563 _logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
564 msg_dict['message_id'] = message_id
566 if 'Subject' in message:
567 msg_dict['subject'] = decode(message.get('Subject'))
569 # Envelope fields not stored in mail.message but made available for message_new()
570 msg_dict['from'] = decode(message.get('from'))
571 msg_dict['to'] = decode(message.get('to'))
572 msg_dict['cc'] = decode(message.get('cc'))
574 if 'From' in message:
575 author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
577 msg_dict['author_id'] = author_ids[0]
578 partner_ids = self._message_find_partners(cr, uid, message, ['From', 'To', 'Cc'], context=context)
579 msg_dict['partner_ids'] = partner_ids
581 if 'Date' in message:
582 date_hdr = decode(message.get('Date'))
583 # convert from email timezone to server timezone
584 date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone()))
585 date_server_datetime_str = date_server_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
586 msg_dict['date'] = date_server_datetime_str
588 if 'In-Reply-To' in message:
589 parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
591 msg_dict['parent_id'] = parent_ids[0]
593 if 'References' in message and 'parent_id' not in msg_dict:
594 parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
595 [x.strip() for x in decode(message['References']).split()])])
597 msg_dict['parent_id'] = parent_ids[0]
599 msg_dict['body'], msg_dict['attachments'] = self._message_extract_payload(message)
602 #------------------------------------------------------
604 #------------------------------------------------------
606 def log(self, cr, uid, id, message, secondary=False, context=None):
607 _logger.warning("log() is deprecated. As this module inherit from "\
608 "mail.thread, the message will be managed by this "\
609 "module instead of by the res.log mechanism. Please "\
610 "use mail_thread.message_post() instead of the "\
611 "now deprecated res.log.")
612 self.message_post(cr, uid, [id], message, context=context)
614 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
615 subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
616 """ Post a new message in an existing thread, returning the new
617 mail.message ID. Extra keyword arguments will be used as default
618 column values for the new mail.message record.
619 Auto link messages for same id and object
620 :param int thread_id: thread ID to post into, or list with one ID
621 :param str body: body of the message, usually raw HTML that will
623 :param str subject: optional subject
624 :param str type: mail_message.type
625 :param int parent_id: optional ID of parent message in this thread
626 :param tuple(str,str) attachments or list id: list of attachment tuples in the form
627 ``(name,content)``, where content is NOT base64 encoded
628 :return: ID of newly created mail.message
630 context = context or {}
631 attachments = attachments or []
632 assert (not thread_id) or isinstance(thread_id, (int, long)) or \
633 (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id"
634 if isinstance(thread_id, (list, tuple)):
635 thread_id = thread_id and thread_id[0]
638 for name, content in attachments:
639 if isinstance(content, unicode):
640 content = content.encode('utf-8')
643 'datas': base64.b64encode(str(content)),
646 'res_model': context.get('thread_model') or self._name,
649 attachment_ids.append((0, 0, data_attach))
653 subtype = 'mail.mt_comment'
654 s = subtype.split('.')
657 ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s[0], s[1])
658 subtype_id = ref and ref[1] or False
660 model = context.get('thread_model', self._name) if thread_id else False
661 messages = self.pool.get('mail.message')
663 #auto link messages for same id and object
664 if self._mail_autothread and thread_id:
665 message_ids = messages.search(cr, uid, ['&',('res_id', '=', thread_id),('model','=',model)], context=context)
667 parent_id = min(message_ids)
673 'res_id': thread_id or False,
675 'subject': subject or False,
677 'parent_id': parent_id,
678 'attachment_ids': attachment_ids,
679 'subtype_id': subtype_id,
682 # if the parent is private, the message must be private
684 msg = messages.browse(cr, uid, parent_id, context=context)
686 values["is_private"] = msg.is_private
688 # Avoid warnings about non-existing fields
689 for x in ('from', 'to', 'cc'):
692 return messages.create(cr, uid, values, context=context)
694 #------------------------------------------------------
696 #------------------------------------------------------
698 def message_post_api(self, cr, uid, thread_id, body='', subject=False, type='notification',
699 subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
700 # if the user write on his wall
701 if self._name=='res.partner' and not thread_id:
702 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
703 thread_id = user.partner_id.id
705 added_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=type,
706 subtype=subtype, parent_id=parent_id, context=context)
710 ir_attachment = self.pool.get('ir.attachment')
711 attachment_ids = ir_attachment.search(cr, 1, [('res_model', '=', ""), ('res_id', '=', ""), ('user_id', '=', uid), ('id', 'in', attachments)], context=context)
713 self.pool.get('ir.attachment').write(cr, 1, attachment_ids, { 'res_model': self._name, 'res_id': thread_id }, context=context)
714 self.pool.get('mail.message').write(cr, 1, [added_message_id], {'attachment_ids': [(6, 0, [pid for pid in attachment_ids])]} )
716 added_message = self.pool.get('mail.message').message_read(cr, uid, [added_message_id])
719 def get_message_subtypes(self, cr, uid, ids, context=None):
720 """ message_subtype_data: data about document subtypes: which are
721 available, which are followed if any """
722 return self._get_subscription_data(cr, uid, ids, None, None, context=context)
724 def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
725 """ Wrapper on message_subscribe, using users. If user_ids is not
726 provided, subscribe uid instead. """
729 partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
730 return self.message_subscribe(cr, uid, ids, partner_ids, subtype_ids=subtype_ids, context=context)
732 def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None):
733 """ Add partners to the records followers. """
734 self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
735 # if subtypes are not specified (and not set to a void list), fetch default ones
736 if subtype_ids is None:
737 subtype_obj = self.pool.get('mail.message.subtype')
738 subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
739 # update the subscriptions
740 fol_obj = self.pool.get('mail.followers')
741 fol_ids = fol_obj.search(cr, 1, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context)
742 fol_obj.write(cr, 1, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
745 def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None):
746 """ Wrapper on message_subscribe, using users. If user_ids is not
747 provided, unsubscribe uid instead. """
750 partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
751 return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context)
753 def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
754 """ Remove partners from the records followers. """
755 return self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
757 #------------------------------------------------------
759 #------------------------------------------------------
761 def message_mark_as_unread(self, cr, uid, ids, context=None):
762 """ Set as unread. """
763 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
765 UPDATE mail_notification SET
768 message_id IN (SELECT id from mail_message where res_id=any(%s) and model=%s limit 1) and
770 ''', (ids, self._name, partner_id))
773 def message_mark_as_read(self, cr, uid, ids, context=None):
775 partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
777 UPDATE mail_notification SET
780 message_id IN (SELECT id FROM mail_message WHERE res_id=ANY(%s) AND model=%s) AND
782 ''', (ids, self._name, partner_id))
785 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: