summaryrefslogtreecommitdiff
path: root/addons/mail/mail_thread.py
diff options
context:
space:
mode:
authorThibault Delavallée <tde@openerp.com>2015-04-27 11:49:02 +0200
committerThibault Delavallée <tde@openerp.com>2015-04-27 12:52:45 +0200
commit4a975f1b7d719ebbf17e0d4194cf3fcc56e2f56f (patch)
tree96a29b7f19adabe5f371b023116da887bb0ace03 /addons/mail/mail_thread.py
parent4b122ad41d873febe288ab1f8c6cdc078ca2a479 (diff)
[MOV] mail: moved files according to the new module organization
Diffstat (limited to 'addons/mail/mail_thread.py')
-rw-r--r--addons/mail/mail_thread.py1966
1 files changed, 0 insertions, 1966 deletions
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
deleted file mode 100644
index 99bc1f41bcc..00000000000
--- a/addons/mail/mail_thread.py
+++ /dev/null
@@ -1,1966 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import base64
-from collections import OrderedDict
-import datetime
-import dateutil
-import email
-try:
- import simplejson as json
-except ImportError:
- import json
-from lxml import etree
-import logging
-import pytz
-import re
-import socket
-import time
-import xmlrpclib
-from email.message import Message
-from email.utils import formataddr
-from urllib import urlencode
-
-from openerp import _, api, fields, models, SUPERUSER_ID
-from openerp import exceptions
-from openerp import tools
-from openerp.addons.mail.mail_message import decode
-from openerp.tools.safe_eval import safe_eval as eval
-
-
-_logger = logging.getLogger(__name__)
-
-
-mail_header_msgid_re = re.compile('<[^<>]+>')
-
-
-def decode_header(message, header, separator=' '):
- return separator.join(map(decode, filter(None, message.get_all(header, []))))
-
-
-class MailThread(models.AbstractModel):
- ''' mail_thread model is meant to be inherited by any model that needs to
- act as a discussion topic on which messages can be attached. Public
- methods are prefixed with ``message_`` in order to avoid name
- collisions with methods of the models that will inherit from this class.
-
- ``mail.thread`` defines fields used to handle and display the
- communication history. ``mail.thread`` also manages followers of
- inheriting classes. All features and expected behavior are managed
- by mail.thread. Widgets has been designed for the 7.0 and following
- versions of OpenERP.
-
- Inheriting classes are not required to implement any method, as the
- default implementation will work for any model. However it is common
- to override at least the ``message_new`` and ``message_update``
- methods (calling ``super``) to add model-specific behavior at
- creation and update of a thread when processing incoming emails.
-
- Options:
- - _mail_flat_thread: if set to True, all messages without parent_id
- are automatically attached to the first message posted on the
- ressource. If set to False, the display of Chatter is done using
- threads, and no parent_id is automatically set.
- '''
- _name = 'mail.thread'
- _description = 'Email Thread'
- _mail_flat_thread = True # flatten the discussino history
- _mail_post_access = 'write' # access required on the document to post on it
- _mail_mass_mailing = False # enable mass mailing on this model
-
- message_is_follower = fields.Boolean('Is Follower', compute='_get_followers', search='_search_is_follower')
- message_follower_ids = fields.Many2many(
- comodel_name='res.partner', string='Followers',
- inverse='_set_followers',
- compute='_get_followers', search='_search_followers', type='many2many')
- message_ids = fields.One2many(
- 'mail.message', 'res_id', string='Messages',
- domain=lambda self: [('model', '=', self._name)], auto_join=True,
- help="Messages and communication history")
- message_last_post = fields.Datetime('Last Message Date', help='Date of the last message posted on the record.')
- message_unread = fields.Boolean(
- 'Unread Messages', compute='_get_message_unread', search='_search_message_unread',
- help="If checked new messages require your attention.")
- message_summary = fields.Text(
- 'Summary', compute='_get_message_unread',
- help="Holds the Chatter summary (number of messages, ...). "\
- "This summary is directly in html format in order to "\
- "be inserted in kanban views.")
-
- @api.multi
- def _get_followers(self):
- res = dict.fromkeys(self.ids, self.env['res.partner'])
- followers = self.env['mail.followers'].sudo().search([('res_model', '=', self._name), ('res_id', 'in', self.ids)])
- for follower in followers:
- res[follower.res_id] |= follower.partner_id
- for record in self:
- record.message_follower_ids = res[record.id]
- record.message_is_follower = self.env.user.partner_id in record.message_follower_ids
-
- @api.multi
- def _set_followers(self):
- # read the old set of followers, and determine the new set of followers
- old = self.env['mail.followers'].sudo().search([('res_model', '=', self._name), ('res_id', 'in', self.ids)]).mapped('partner_id')
- new = self.env['res.partner']
- Partner = self.env['res.partner']
-
- old_style_commands = self._fields['message_follower_ids'].convert_to_write(self.message_follower_ids)
- for command in old_style_commands:
- if command[0] == 0:
- new |= Partner.create(command[2])
- elif command[0] == 1:
- partner = Partner.browse(command[1])
- partner.write(command[2])
- new |= partner
- elif command[0] == 2:
- partner = Partner.browse(command[1])
- new -= partner
- partner.unlink()
- elif command[0] == 3:
- new -= Partner.browse(command[1])
- elif command[0] == 4:
- new |= Partner.browse(command[1])
- elif command[0] == 5:
- new = self.env['res.partner']
- elif command[0] == 6:
- new = self.env['res.partner'].browse(command[2])
-
- # remove partners that are no longer followers
- self.message_unsubscribe((old-new).ids)
- # add new followers
- self.message_subscribe((new-old).ids)
-
- @api.model
- def _search_followers(self, operator, operand):
- """Search function for message_follower_ids
-
- Do not use with operator 'not in'. Use instead message_is_followers
- """
- # TOFIX make it work with not in
- assert operator != "not in", "Do not search message_follower_ids with 'not in'"
- followers = self.env['mail.followers'].sudo().search([('res_model', '=', self._name), ('partner_id', operator, operand)])
- return [('id', 'in', followers.mapped('res_id'))]
-
- @api.model
- def _search_is_follower(self, operator, operand):
- """Search function for message_is_follower"""
- user_pid = self.env.user.partner_id.id
- if (operator == '=' and operand) or (operator == '!=' and not operand): # is a follower
- followers = self.env['mail.followers'].sudo().search([('res_model', '=', self._name), ('partner_id', '=', user_pid)])
- return [('id', 'in', followers.mapped('res_id'))]
- else: # is not a follower or unknown domain
- followers = self.env['mail.followers'].sudo().search([('res_model', '=', self._name), ('partner_id', '=', user_pid)])
- return [('id', 'not in', followers.mapped('res_id'))]
-
- @api.multi
- def _get_message_unread(self):
- """ Compute the existence of unread message (message_unread) + the kanban
- summary for unread messages (message_summary) """
- res = dict((res_id, 0) for res_id in self.ids)
-
- # search for unread messages, directly in SQL to improve performances
- self._cr.execute(""" SELECT m.res_id FROM mail_message m
- RIGHT JOIN mail_notification n
- ON (n.message_id = m.id AND n.partner_id = %s AND (n.is_read = False or n.is_read IS NULL))
- WHERE m.model = %s AND m.res_id in %s""",
- (self.env.user.partner_id.id, self._name, tuple(self.ids),))
- for result in self._cr.fetchall():
- res[result[0]] += 1
-
- for record in self:
- record.message_unread = res.get(record.id, 0) >= 1
- if record.message_unread:
- record.message_summary = "<span class='oe_kanban_mail_new' title='%(title)s'><i class='fa fa-comments'/> %(count)d</span>" % {
- 'title': '%d%s' % (res[record.id], _('unread messages')),
- 'count': res[record.id]}
- else:
- record.message_summary = ''
-
- @api.model
- def _search_message_unread(self, operator, operand):
- return [('message_ids.to_read', operator, operand)]
-
- # ------------------------------------------------------
- # CRUD overrides for automatic subscription and logging
- # ------------------------------------------------------
-
- @api.model
- def create(self, values):
- """ Chatter override :
- - subscribe uid
- - subscribe followers of parent
- - log a creation message
- """
- if self._context.get('tracking_disable'):
- return super(MailThread, self).create(values)
-
- # subscribe uid unless asked not to
- if not self._context.get('mail_create_nosubscribe'):
- message_follower_ids = values.get('message_follower_ids') or [] # webclient can send None or False
- message_follower_ids.append([4, self.env.user.partner_id.id])
- values['message_follower_ids'] = message_follower_ids
- thread = super(MailThread, self).create(values)
-
- # automatic logging unless asked not to (mainly for various testing purpose)
- if not self._context.get('mail_create_nolog'):
- doc_name = self.env['ir.model'].search([('model', '=', self._name)]).read(['name'])[0]['name']
- thread.message_post(body=_('%s created') % doc_name)
-
- # auto_subscribe: take values and defaults into account
- create_values = dict(values)
- for key, val in self._context.iteritems():
- if key.startswith('default_') and key[8:] not in create_values:
- create_values[key[8:]] = val
- thread.message_auto_subscribe(create_values.keys(), values=create_values)
-
- # track values
- if not self._context.get('mail_notrack'):
- if 'lang' not in self._context:
- track_thread = thread.with_context(lang=self.env.user.lang)
- else:
- track_thread = thread
- tracked_fields = track_thread._get_tracked_fields(values.keys())
- if tracked_fields:
- initial_values = {thread.id: dict.fromkeys(tracked_fields, False)}
- track_thread.message_track(tracked_fields, initial_values)
- return thread
-
- @api.multi
- def write(self, values):
- if self._context.get('tracking_disable'):
- return super(MailThread, self).write(values)
-
- # Track initial values of tracked fields
- if 'lang' not in self._context:
- track_self = self.with_context(lang=self.env.user.lang)
- else:
- track_self = self
-
- tracked_fields = None
- if not self._context.get('mail_notrack'):
- tracked_fields = track_self._get_tracked_fields(values.keys())
- if tracked_fields:
- initial_values = dict((record.id, dict((key, getattr(record, key)) for key in tracked_fields))
- for record in track_self)
-
- # Perform write, update followers
- result = super(MailThread, self).write(values)
- self.message_auto_subscribe(values.keys(), values=values)
-
- # Perform the tracking
- if tracked_fields:
- track_self.message_track(tracked_fields, initial_values)
-
- return result
-
- @api.multi
- def unlink(self):
- """ Override unlink to delete messages and followers. This cannot be
- cascaded, because link is done through (res_model, res_id). """
- self.env['mail.message'].search([('model', '=', self._name), ('res_id', 'in', self.ids)]).unlink()
- res = super(MailThread, self).unlink()
- self.env['mail.followers'].sudo().search(
- [('res_model', '=', self._name), ('res_id', 'in', self.ids)]
- ).unlink()
- return res
-
- def copy_data(self, cr, uid, id, default=None, context=None):
- context = dict(context or {}, mail_notrack=True)
- # avoid tracking multiple temporary changes during copy
- return super(MailThread, self).copy_data(cr, uid, id, default=default, context=context)
-
- # ------------------------------------------------------
- # Technical methods (to clean / move to controllers ?)
- # ------------------------------------------------------
-
- @api.model
- def get_empty_list_help(self, help):
- """ Override of BaseModel.get_empty_list_help() to generate an help message
- that adds alias information. """
- model = self._context.get('empty_list_help_model')
- res_id = self._context.get('empty_list_help_id')
- catchall_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
- document_name = self._context.get('empty_list_help_document_name', _('document'))
- add_arrow = not help or help.find("oe_view_nocontent_create") == -1
- alias = None
-
- if catchall_domain and model and res_id: # specific res_id -> find its alias (i.e. section_id specified)
- record = self.env[model].sudo().browse(res_id)
- # check that the alias effectively creates new records
- if record.alias_id and record.alias_id.alias_name and \
- record.alias_id.alias_model_id and \
- record.alias_id.alias_model_id.model == self._name and \
- record.alias_id.alias_force_thread_id == 0:
- alias = record.alias_id
- if not alias and catchall_domain and model: # no res_id or res_id not linked to an alias -> generic help message, take a generic alias of the model
- Alias = self.env['mail.alias']
- aliases = Alias.search([
- ("alias_parent_model_id.model", "=", model),
- ("alias_name", "!=", False),
- ('alias_force_thread_id', '=', False),
- ('alias_parent_thread_id', '=', False)], order='id ASC')
- if aliases and len(aliases) == 1:
- alias = aliases[0]
-
- if alias:
- email_link = "<a href='mailto:%(email)s'>%(email)s</a>" % {'email': alias.name_get()[0][1]}
- if add_arrow:
- return "<p class='oe_view_nocontent_create'>%(dyn_help)s</p>%(static_help)s" % {
- 'static_help': help or '',
- 'dyn_help': _("Click here to add new %(document)s or send an email to: %(email_link)s") % {
- 'document': document_name,
- 'email_link': email_link
- }
- }
- return "%(static_help)s<p>%(dyn_help)s" % {
- 'static_help': help or '',
- 'dyn_help': _("You could also add a new %(document)s by sending an email to: %(email_link)s.") % {
- 'document': document_name,
- 'email_link': email_link,
- }
- }
-
- if add_arrow:
- return "<p class='oe_view_nocontent_create'>%(dyn_help)s</p>%(static_help)s" % {
- 'static_help': help or '',
- 'dyn_help': _("Click here to add new %s") % document_name,
- }
-
- return help
-
- def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
- res = super(MailThread, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
- if view_type == 'form':
- doc = etree.XML(res['arch'])
- for node in doc.xpath("//field[@name='message_ids']"):
- options = json.loads(node.get('options', '{}'))
- user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
- options['display_log_button'] = user.has_group('base.group_user')
- node.set('options', json.dumps(options))
- res['arch'] = etree.tostring(doc)
- return res
-
- @api.model
- def read_followers_data(self, follower_ids):
- # TDE NOTE: move as controller ? (used in mail_followers.js)$
- # TDE NOTE2: usefull anyway ?
- result = []
- is_editable = self.env.user.has_group('base.group_no_one')
- for follower in self.env['res.partner'].browse(follower_ids):
- is_uid = self.env.user.partner_id.id in follower.ids
- data = (follower.id,
- follower.name,
- {'is_editable': is_editable, 'is_uid': is_uid},
- )
- result.append(data)
- return result
-
- @api.multi
- def _get_subscription_data(self, user_pid=None):
- # TDE NOTE: move as controller ? (used in mail_followers.js)
- """ Computes:
- - message_subtype_data: data about document subtypes: which are
- available, which are followed if any """
- res = dict((id, dict(message_subtype_data='')) for id in self.ids)
- if user_pid is None:
- user_pid = self.env.user.partner_id.id
-
- # find current model subtypes, add them to a dictionary
- subtypes = self.env['mail.message.subtype'].search(['&', ('hidden', '=', False), '|', ('res_model', '=', self._name), ('res_model', '=', False)])
- subtype_dict = OrderedDict(
- (subtype.name, {
- 'default': subtype.default,
- 'followed': False,
- 'parent_model': subtype.parent_id and subtype.parent_id.res_model or self._name,
- 'id': subtype.id
- }) for subtype in subtypes)
- for res_id in self.ids:
- res[res_id]['message_subtype_data'] = subtype_dict.copy()
-
- # find the document followers, update the data
- followers = self.env['mail.followers'].search([
- ('partner_id', '=', user_pid),
- ('res_id', 'in', self.ids),
- ('res_model', '=', self._name),
- ])
- for fol in followers:
- thread_subtype_dict = res[fol.res_id]['message_subtype_data']
- for subtype in [st for st in fol.subtype_ids if st.name in thread_subtype_dict]:
- thread_subtype_dict[subtype.name]['followed'] = True
- res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
-
- return res
-
- # ------------------------------------------------------
- # Automatic log / Tracking
- # ------------------------------------------------------
-
- @api.model
- def _get_tracked_fields(self, updated_fields):
- """ Return a structure of tracked fields for the current model.
- :param list updated_fields: modified field names
- :return dict: a dict mapping field name to description, containing
- always tracked fields and modified on_change fields
- """
- tracked_fields = []
- for name, field in self._fields.items():
- if getattr(field, 'track_visibility', False):
- tracked_fields.append(name)
-
- if tracked_fields:
- return self.fields_get(tracked_fields)
- return {}
-
- @api.multi
- def _track_subtype(self, init_values):
- """ Give the subtypes triggered by the changes on the record according
- to values that have been updated.
-
- :param ids: list of a single ID, the ID of the record being modified
- :type ids: singleton list
- :param init_values: the original values of the record; only modified fields
- are present in the dict
- :type init_values: dict
- :returns: a subtype xml_id or False if no subtype is trigerred
- """
- return False
-
- @api.multi
- def message_track(self, tracked_fields, initial_values):
-
- def convert_for_display(value, col_info):
- if not value and col_info['type'] == 'boolean':
- return 'False'
- if not value:
- return ''
- if col_info['type'] == 'many2one':
- return value.name_get()[0][1]
- if col_info['type'] == 'selection':
- return dict(col_info['selection'])[value]
- return value
-
- def format_message(message_description, tracked_values):
- message = ''
- if message_description:
- message = '<span>%s</span>' % message_description
- for name, change in tracked_values.items():
- message += '<div> &nbsp; &nbsp; &bull; <b>%s</b>: ' % change.get('col_info')
- if change.get('old_value'):
- message += '%s &rarr; ' % change.get('old_value')
- message += '%s</div>' % change.get('new_value')
- return message
-
- if not tracked_fields:
- return True
-
- for record in self:
- initial = initial_values[record.id]
- changes = set()
- tracked_values = {}
-
- # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
- for col_name, col_info in tracked_fields.items():
- field = self._fields[col_name]
- initial_value = initial[col_name]
- record_value = getattr(record, col_name)
-
- if record_value == initial_value and getattr(field, 'track_visibility', None) == 'always':
- tracked_values[col_name] = dict(
- col_info=col_info['string'],
- new_value=convert_for_display(record_value, col_info),
- )
- elif record_value != initial_value and (record_value or initial_value): # because browse null != False
- if getattr(field, 'track_visibility', None) in ['always', 'onchange']:
- tracked_values[col_name] = dict(
- col_info=col_info['string'],
- old_value=convert_for_display(initial_value, col_info),
- new_value=convert_for_display(record_value, col_info),
- )
- if col_name in tracked_fields:
- changes.add(col_name)
- if not changes:
- continue
-
- # find subtypes and post messages or log if no subtype found
- subtype_xmlid = False
- # By passing this key, that allows to let the subtype empty and so don't sent email because partners_to_notify from mail_message._notify will be empty
- if not self._context.get('mail_track_log_only'):
- subtype_xmlid = record._track_subtype(dict((col_name, initial[col_name]) for col_name in changes))
- # compatibility: use the deprecated _track dict
- if not subtype_xmlid and hasattr(self, '_track'):
- for field, track_info in self._track.items():
- if field not in changes or subtype_xmlid:
- continue
- for subtype, method in track_info.items():
- if method(self, self._cr, self._uid, record, self._context):
- _logger.warning("Model %s still using deprecated _track dict; override _track_subtype method instead" % self._name)
- subtype_xmlid = subtype
-
- if subtype_xmlid:
- subtype_rec = self.env.ref(subtype_xmlid) # TDE FIXME check for raise if not found
- if not (subtype_rec and subtype_rec.exists()):
- _logger.debug('subtype %s not found' % subtype_xmlid)
- continue
- message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
- else:
- message = format_message('', tracked_values)
- record.message_post(body=message, subtype=subtype_xmlid)
- return True
-
- #------------------------------------------------------
- # mail.message wrappers and tools
- #------------------------------------------------------
-
- @api.model
- def _needaction_domain_get(self):
- if self._needaction:
- return [('message_unread', '=', True)]
- return []
-
- @api.model
- def _garbage_collect_attachments(self):
- """ Garbage collect lost mail attachments. Those are attachments
- - linked to res_model 'mail.compose.message', the composer wizard
- - with res_id 0, because they were created outside of an existing
- wizard (typically user input through Chatter or reports
- created on-the-fly by the templates)
- - unused since at least one day (create_date and write_date)
- """
- limit_date = datetime.datetime.utcnow() - datetime.timedelta(days=1)
- limit_date_str = datetime.datetime.strftime(limit_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
- self.env['ir.attachment'].search([
- ('res_model', '=', 'mail.compose.message'),
- ('res_id', '=', 0),
- ('create_date', '<', limit_date_str),
- ('write_date', '<', limit_date_str)]
- ).unlink()
- return True
-
- @api.model
- def check_mail_message_access(self, res_ids, operation, model_name=None):
- """ mail.message check permission rules for related document. This method is
- meant to be inherited in order to implement addons-specific behavior.
- A common behavior would be to allow creating messages when having read
- access rule on the document, for portal document such as issues. """
- if model_name:
- DocModel = self.env[model_name]
- else:
- DocModel = self
- if hasattr(DocModel, '_mail_post_access'):
- create_allow = DocModel._mail_post_access
- else:
- create_allow = 'write'
-
- if operation in ['write', 'unlink']:
- check_operation = 'write'
- elif operation == 'create' and create_allow in ['create', 'read', 'write', 'unlink']:
- check_operation = create_allow
- elif operation == 'create':
- check_operation = 'write'
- else:
- check_operation = operation
-
- DocModel.check_access_rights(check_operation)
- DocModel.browse(res_ids).check_access_rule(check_operation)
-
- @api.model
- def _get_inbox_action_xml_id(self):
- """ When redirecting towards the Inbox, choose which action xml_id has
- to be fetched. This method is meant to be inherited, at least in portal
- because portal users have a different Inbox action than classic users. """
- return 'mail.action_mail_inbox_feeds'
-
- @api.model
- def message_redirect_action(self):
- """ For a given message, return an action that either
- - opens the form view of the related document if model, res_id, and
- read access to the document
- - opens the Inbox with a default search on the conversation if model,
- res_id
- - opens the Inbox with context propagated
-
- """
- # default action is the Inbox action
- action = self.env.ref(self._get_inbox_action_xml_id()).read()[0]
- params = self._context.get('params')
- msg_id = model = res_id = None
-
- if params:
- msg_id = params.get('message_id')
- model = params.get('model')
- res_id = params.get('res_id', params.get('id')) # signup automatically generated id instead of res_id
- if not msg_id and not (model and res_id):
- return action
- if msg_id and not (model and res_id):
- msg = self.env['mail.message'].browse(msg_id).exists()
- try:
- model, res_id = msg.model, msg.res_id
- except exceptions.AccessError:
- pass
-
- # if model + res_id found: try to redirect to the document or fallback on the Inbox
- if model and res_id:
- RecordModel = self.env[model]
- if RecordModel.check_access_rights('read', raise_exception=False):
- try:
- # TDE FIXME: clean that copde
- RecordModel.browse(res_id).check_access_rule('read')
- action = RecordModel.browse(res_id).get_access_action()[0]
- except exceptions.AccessError:
- pass
- action.update({
- 'context': {
- 'search_default_model': model,
- 'search_default_res_id': res_id,
- }
- })
- return action
-
- @api.model
- def _get_access_link(self, mail, partner):
- # the parameters to encode for the query and fragment part of url
- query = {'db': self._cr.dbname}
- fragment = {
- 'login': partner.user_ids[0].login,
- 'action': 'mail.action_mail_redirect',
- }
- if mail.notification:
- fragment['message_id'] = mail.mail_message_id.id
- elif mail.model and mail.res_id:
- fragment.update(model=mail.model, res_id=mail.res_id)
-
- return "/web?%s#%s" % (urlencode(query), urlencode(fragment))
-
- # ------------------------------------------------------
- # Email specific
- # ------------------------------------------------------
-
- @api.multi
- def message_get_default_recipients(self, res_model=None, res_ids=None):
- if res_model and res_ids:
- if hasattr(self.env[res_model], 'message_get_default_recipients'):
- return self.env[res_model].browse(res_ids).message_get_default_recipients()
- records = self.env[res_model].sudo().browse(res_ids)
- else:
- records = self.sudo()
- res = {}
- for record in records:
- recipient_ids, email_to, email_cc = set(), False, False
- if 'partner_id' in self._fields and record.partner_id:
- recipient_ids.add(record.partner_id.id)
- elif 'email_from' in self._fields and record.email_from:
- email_to = record.email_from
- elif 'email' in self._fields:
- email_to = record.email
- res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc}
- return res
-
- @api.model
- def message_get_reply_to(self, res_ids, default=None):
- """ Returns the preferred reply-to email address that is basically the
- alias of the document, if it exists. Override this method to implement
- a custom behavior about reply-to for generated emails. """
- model_name = self.env.context.get('thread_model') or self._name
- alias_domain = self.env['ir.config_parameter'].get_param("mail.catchall.domain")
- res = dict.fromkeys(res_ids, False)
-
- # alias domain: check for aliases and catchall
- aliases = {}
- doc_names = {}
- if alias_domain:
- if model_name and model_name != 'mail.thread' and res_ids:
- mail_aliases = self.env['mail.alias'].sudo().search([
- ('alias_parent_model_id.model', '=', model_name),
- ('alias_parent_thread_id', 'in', res_ids),
- ('alias_name', '!=', False)])
- aliases.update(
- dict((alias.alias_parent_thread_id, '%s@%s' % (alias.alias_name, alias_domain)) for alias in mail_aliases))
- doc_names.update(
- dict((ng_res[0], ng_res[1])
- for ng_res in self.env[model_name].sudo().browse(aliases.keys()).name_get()))
- # left ids: use catchall
- left_ids = set(res_ids).difference(set(aliases.keys()))
- if left_ids:
- catchall_alias = self.env['ir.config_parameter'].get_param("mail.catchall.alias")
- if catchall_alias:
- aliases.update(dict((res_id, '%s@%s' % (catchall_alias, alias_domain)) for res_id in left_ids))
- # compute name of reply-to
- company_name = self.env.user.company_id.name
- for res_id in aliases.keys():
- email_name = '%s%s' % (company_name, doc_names.get(res_id) and (' ' + doc_names[res_id]) or '')
- email_addr = aliases[res_id]
- res[res_id] = formataddr((email_name, email_addr))
- left_ids = set(res_ids).difference(set(aliases.keys()))
- if left_ids:
- res.update(dict((res_id, default) for res_id in res_ids))
- return res
-
- @api.multi
- def message_get_email_values(self, notif_mail=None):
- """ Get specific notification email values to store on the notification
- mail_mail. Void method, inherit it to add custom values. """
- self.ensure_one()
- res = dict()
- return res
-
- # ------------------------------------------------------
- # Mail gateway
- # ------------------------------------------------------
-
- @api.model
- def message_capable_models(self):
- """ Used by the plugin addon, based for plugin_outlook and others. """
- ret_dict = {}
- for model_name in self.pool.obj_list():
- model = self.pool[model_name]
- if hasattr(model, "message_process") and hasattr(model, "message_post"):
- ret_dict[model_name] = model._description
- return ret_dict
-
- def _message_find_partners(self, message, header_fields=['From']):
- """ Find partners related to some header fields of the message.
-
- :param string message: an email.message instance """
- s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
- return filter(lambda x: x, self._find_partner_from_emails(tools.email_split(s)))
-
- @api.model
- def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False):
- """ Verify route validity. Check and rules:
- 1 - if thread_id -> check that document effectively exists; otherwise
- fallback on a message_new by resetting thread_id
- 2 - check that message_update exists if thread_id is set; or at least
- that message_new exist
- [ - find author_id if udpate_author is set]
- 3 - if there is an alias, check alias_contact:
- 'followers' and thread_id:
- check on target document that the author is in the followers
- 'followers' and alias_parent_thread_id:
- check on alias parent document that the author is in the
- followers
- 'partners': check that author_id id set
- """
-
- assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
- assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
-
- message_id = message.get('Message-Id')
- email_from = decode_header(message, 'From')
- author_id = message_dict.get('author_id')
- model, thread_id, alias = route[0], route[1], route[4]
- record_set = None
-
- def _create_bounce_email():
- self.env['mail.mail'].create({
- 'body_html': '<div><p>Hello,</p>'
- '<p>The following email sent to %s cannot be accepted because this is '
- 'a private email address. Only allowed people can contact us at this address.</p></div>'
- '<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')),
- 'subject': 'Re: %s' % message.get('subject'),
- 'email_to': message.get('from'),
- 'auto_delete': True,
- }).send()
-
- def _warn(message):
- _logger.info('Routing mail with Message-Id %s: route %s: %s',
- message_id, route, message)
-
- # Wrong model
- if model and model not in self.pool:
- if assert_model:
- assert model in self.pool, 'Routing: unknown target model %s' % model
- _warn('unknown target model %s' % model)
- return ()
-
- # Private message: should not contain any thread_id
- if not model and thread_id:
- if assert_model:
- if thread_id:
- raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id)
- _warn('posting a message without model should be with a null res_id (private message), received %s resetting thread_id' % thread_id)
- thread_id = 0
- # Private message: should have a parent_id (only answers)
- if not model and not message_dict.get('parent_id'):
- if assert_model:
- if not message_dict.get('parent_id'):
- raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).')
- _warn('posting a message without model should be with a parent_id (private mesage), skipping')
- return ()
-
- if model and thread_id:
- record_set = self.env[model].browse(thread_id)
- elif model:
- record_set = self.env[model]
-
- # Existing Document: check if exists; if not, fallback on create if allowed
- if thread_id and not record_set.exists():
- if create_fallback:
- _warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id))
- thread_id = None
- elif assert_model:
- # TDE FIXME: change assert to some real error
- assert record_set.exists(), 'Routing: reply to missing document (%s,%s)' % (model, thread_id)
- else:
- _warn('reply to missing document (%s,%s), skipping' % (model, thread_id))
- return ()
-
- # Existing Document: check model accepts the mailgateway
- if thread_id and model and not hasattr(record_set, 'message_update'):
- if create_fallback:
- _warn('model %s does not accept document update, fall back on document creation' % model)
- thread_id = None
- elif assert_model:
- assert hasattr(record_set, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model
- else:
- _warn('model %s does not accept document update, skipping' % model)
- return ()
-
- # New Document: check model accepts the mailgateway
- if not thread_id and model and not hasattr(record_set, 'message_new'):
- if assert_model:
- if not hasattr(record_set, 'message_new'):
- raise ValueError(
- 'Model %s does not accept document creation, crashing' % model
- )
- _warn('model %s does not accept document creation, skipping' % model)
- return ()
-
- # Update message author if asked
- # We do it now because we need it for aliases (contact settings)
- if not author_id and update_author:
- author_ids = self.env['mail.thread']._find_partner_from_emails([email_from], res_model=model, res_id=thread_id)
- if author_ids:
- author_id = author_ids[0]
- message_dict['author_id'] = author_id
-
- # Alias: check alias_contact settings
- if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id):
- if thread_id:
- obj = record_set[0]
- else:
- obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id)
- if not author_id or author_id not in [fol.id for fol in obj.message_follower_ids]:
- _warn('alias %s restricted to internal followers, skipping' % alias.alias_name)
- _create_bounce_email()
- return ()
- elif alias and alias.alias_contact == 'partners' and not author_id:
- _warn('alias %s does not accept unknown author, skipping' % alias.alias_name)
- _create_bounce_email()
- return ()
-
- if not model and not thread_id and not alias and not allow_private:
- return ()
-
- return (model, thread_id, route[2], route[3], route[4])
-
- @api.model
- def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
- """Attempt to figure out the correct target model, thread_id,
- custom_values and user_id to use for an incoming message.
- Multiple values may be returned, if a message had multiple
- recipients matching existing mail.aliases, for example.
-
- The following heuristics are used, in this order:
- 1. If the message replies to an existing thread_id, and
- properly contains the thread model in the 'In-Reply-To'
- header, use this model/thread_id pair, and ignore
- custom_value (not needed as no creation will take place)
- 2. Look for a mail.alias entry matching the message
- recipient, and use the corresponding model, thread_id,
- custom_values and user_id.
- 3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
- provided.
- 4. If all the above fails, raise an exception.
-
- :param string message: an email.message instance
- :param dict message_dict: dictionary holding message variables
- :param string model: the fallback model to use if the message
- does not match any of the currently configured mail aliases
- (may be None if a matching alias is supposed to be present)
- :type dict custom_values: optional dictionary of default field values
- to pass to ``message_new`` if a new record needs to be created.
- Ignored if the thread record already exists, and also if a
- matching mail.alias was found (aliases define their own defaults)
- :param int thread_id: optional ID of the record/thread from ``model``
- to which this mail should be attached. Only used if the message
- does not reply to an existing thread and does not match any mail alias.
- :return: list of [model, thread_id, custom_values, user_id, alias]
-
- :raises: ValueError, TypeError
- """
- if not isinstance(message, Message):
- raise TypeError('message must be an email.message.Message at this point')
- MailMessage = self.env['mail.message']
- fallback_model = model
-
- # Get email.message.Message variables for future processing
- message_id = message.get('Message-Id')
- email_from = decode_header(message, 'From')
- email_to = decode_header(message, 'To')
- references = decode_header(message, 'References')
- in_reply_to = decode_header(message, 'In-Reply-To')
- thread_references = references or in_reply_to
-
- # 0. First check if this is a bounce message or not.
- # See http://datatracker.ietf.org/doc/rfc3462/?include_text=1
- # As all MTA does not respect this RFC (googlemail is one of them),
- # we also need to verify if the message come from "mailer-daemon"
- localpart = (tools.email_split(email_from) or [''])[0].split('@', 1)[0].lower()
- if message.get_content_type() == 'multipart/report' or localpart == 'mailer-daemon':
- _logger.info("Not routing bounce email from %s to %s with Message-Id %s",
- email_from, email_to, message_id)
- return []
-
- # 1. message is a reply to an existing message (exact match of message_id)
- ref_match = thread_references and tools.reference_re.search(thread_references)
- msg_references = mail_header_msgid_re.findall(thread_references)
- mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
- if ref_match and mail_messages:
- model, thread_id = mail_messages.model, mail_messages.res_id
- route = self.message_route_verify(
- message, message_dict,
- (model, thread_id, custom_values, self._uid, None),
- update_author=True, assert_model=False, create_fallback=True)
- if route:
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
- return [route]
-
- # 2. message is a reply to an existign thread (6.1 compatibility)
- if ref_match:
- reply_thread_id = int(ref_match.group(1))
- reply_model = ref_match.group(2) or fallback_model
- reply_hostname = ref_match.group(3)
- local_hostname = socket.gethostname()
- # do not match forwarded emails from another OpenERP system (thread_id collision!)
- if local_hostname == reply_hostname:
- thread_id, model = reply_thread_id, reply_model
- if thread_id and model in self.pool:
- record = self.env[model].browse(thread_id)
- compat_mail_msg_ids = MailMessage.search([
- ('message_id', '=', False),
- ('model', '=', model),
- ('res_id', '=', thread_id)])
- if compat_mail_msg_ids and record.exists() and hasattr(record, 'message_update'):
- route = self.message_route_verify(
- message, message_dict,
- (model, thread_id, custom_values, self._uid, None),
- update_author=True, assert_model=True, create_fallback=True)
- if route:
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
- return [route]
-
- # 3. Reply to a private message
- if in_reply_to:
- mail_message_ids = MailMessage.search([
- ('message_id', '=', in_reply_to),
- '!', ('message_id', 'ilike', 'reply_to')
- ], limit=1)
- if mail_message_ids:
- route = self.message_route_verify(
- message, message_dict,
- (mail_message_ids.model, mail_message_ids.res_id, custom_values, self._uid, None),
- update_author=True, assert_model=True, create_fallback=True, allow_private=True)
- if route:
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, mail_message_ids.id, custom_values, self._uid)
- return [route]
-
- # 4. Look for a matching mail.alias entry
- # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
- # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
- rcpt_tos = \
- ','.join([decode_header(message, 'Delivered-To'),
- decode_header(message, 'To'),
- decode_header(message, 'Cc'),
- decode_header(message, 'Resent-To'),
- decode_header(message, 'Resent-Cc')])
- local_parts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)]
- if local_parts:
- Alias = self.env['mail.alias']
- aliases = Alias.search([('alias_name', 'in', local_parts)])
- if aliases:
- routes = []
- for alias in aliases:
- user_id = alias.alias_user_id.id
- if not user_id:
- # TDE note: this could cause crashes, because no clue that the user
- # that send the email has the right to create or modify a new document
- # Fallback on user_id = uid
- # Note: recognized partners will be added as followers anyway
- # user_id = self._message_find_user_id(cr, uid, message, context=context)
- user_id = self._uid
- _logger.info('No matching user_id for the alias %s', alias.alias_name)
- route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)
- route = self.message_route_verify(
- message, message_dict, route,
- update_author=True, assert_model=True, create_fallback=True)
- if route:
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
- email_from, email_to, message_id, route)
- routes.append(route)
- return routes
-
- # 5. Fallback to the provided parameters, if they work
- if not thread_id:
- # Legacy: fallback to matching [ID] in the Subject
- match = tools.res_re.search(decode_header(message, 'Subject'))
- thread_id = match and match.group(1)
- # Convert into int (bug spotted in 7.0 because of str)
- try:
- thread_id = int(thread_id)
- except:
- thread_id = False
- route = self.message_route_verify(
- message, message_dict,
- (fallback_model, thread_id, custom_values, self._uid, None),
- update_author=True, assert_model=True)
- if route:
- _logger.info(
- 'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
- email_from, email_to, message_id, fallback_model, thread_id, custom_values, self._uid)
- return [route]
-
- # ValueError if no routes found and if no bounce occured
- raise ValueError(
- 'No possible route found for incoming message from %s to %s (Message-Id %s:). '
- 'Create an appropriate mail.alias or force the destination model.' %
- (email_from, email_to, message_id)
- )
-
- @api.model
- def message_route_process(self, message, message_dict, routes):
- # postpone setting message_dict.partner_ids after message_post, to avoid double notifications
- partner_ids = message_dict.pop('partner_ids', [])
- thread_id = False
- for model, thread_id, custom_values, user_id, alias in routes:
- if model:
- Model = self.env[model]
- if not (thread_id and hasattr(Model, 'message_update') or hasattr(Model, 'message_new')):
- raise ValueError(
- "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" %
- (message_dict['message_id'], model)
- )
-
- # disabled subscriptions during message_new/update to avoid having the system user running the
- # email gateway become a follower of all inbound messages
- MessageModel = Model.sudo(user_id).with_context(mail_create_nosubscribe=True, mail_create_nolog=True)
- if thread_id and hasattr(MessageModel, 'message_update'):
- MessageModel.browse(thread_id).message_update(message_dict)
- else:
- thread_id = MessageModel.message_new(message_dict, custom_values)
- else:
- if thread_id:
- raise ValueError("Posting a message without model should be with a null res_id, to create a private message.")
- Model = self.env['mail.thread']
- if not hasattr(Model, 'message_post'):
- Model = self.env['mail.thread'].with_context(thread_model=model)
- new_msg = Model.browse(thread_id).message_post(subtype='mail.mt_comment', **message_dict)
-
- if partner_ids:
- # postponed after message_post, because this is an external message and we don't want to create
- # duplicate emails due to notifications
- new_msg.write({'partner_ids': partner_ids})
- return thread_id
-
- @api.model
- def message_process(self, model, message, custom_values=None,
- save_original=False, strip_attachments=False,
- thread_id=None):
- """ Process an incoming RFC2822 email message, relying on
- ``mail.message.parse()`` for the parsing operation,
- and ``message_route()`` to figure out the target model.
-
- Once the target model is known, its ``message_new`` method
- is called with the new message (if the thread record did not exist)
- or its ``message_update`` method (if it did).
-
- There is a special case where the target model is False: a reply
- to a private message. In this case, we skip the message_new /
- message_update step, to just post a new message using mail_thread
- message_post.
-
- :param string model: the fallback model to use if the message
- does not match any of the currently configured mail aliases
- (may be None if a matching alias is supposed to be present)
- :param message: source of the RFC2822 message
- :type message: string or xmlrpclib.Binary
- :type dict custom_values: optional dictionary of field values
- to pass to ``message_new`` if a new record needs to be created.
- Ignored if the thread record already exists, and also if a
- matching mail.alias was found (aliases define their own defaults)
- :param bool save_original: whether to keep a copy of the original
- email source attached to the message after it is imported.
- :param bool strip_attachments: whether to strip all attachments
- before processing the message, in order to save some space.
- :param int thread_id: optional ID of the record/thread from ``model``
- to which this mail should be attached. When provided, this
- overrides the automatic detection based on the message
- headers.
- """
- # extract message bytes - we are forced to pass the message as binary because
- # we don't know its encoding until we parse its headers and hence can't
- # convert it to utf-8 for transport between the mailgate script and here.
- if isinstance(message, xmlrpclib.Binary):
- message = str(message.data)
- # Warning: message_from_string doesn't always work correctly on unicode,
- # we must use utf-8 strings here :-(
- if isinstance(message, unicode):
- message = message.encode('utf-8')
- msg_txt = email.message_from_string(message)
-
- # parse the message, verify we are not in a loop by checking message_id is not duplicated
- msg = self.message_parse(msg_txt, save_original=save_original)
- if strip_attachments:
- msg.pop('attachments', None)
-
- if msg.get('message_id'): # should always be True as message_parse generate one if missing
- existing_msg_ids = self.env['mail.message'].search([('message_id', '=', msg.get('message_id'))])
- if existing_msg_ids:
- _logger.info('Ignored mail from %s to %s with Message-Id %s: found duplicated Message-Id during processing',
- msg.get('from'), msg.get('to'), msg.get('message_id'))
- return False
-
- # find possible routes for the message
- routes = self.message_route(msg_txt, msg, model, thread_id, custom_values)
- thread_id = self.message_route_process(msg_txt, msg, routes)
- return thread_id
-
- @api.model
- def message_new(self, msg_dict, custom_values=None):
- """Called by ``message_process`` when a new message is received
- for a given thread model, if the message did not belong to
- an existing thread.
- The default behavior is to create a new record of the corresponding
- model (based on some very basic info extracted from the message).
- Additional behavior may be implemented by overriding this method.
-
- :param dict msg_dict: a map containing the email details and
- attachments. See ``message_process`` and
- ``mail.message.parse`` for details.
- :param dict custom_values: optional dictionary of additional
- field values to pass to create()
- when creating the new thread record.
- Be careful, these values may override
- any other values coming from the message.
- :param dict context: if a ``thread_model`` value is present
- in the context, its value will be used
- to determine the model of the record
- to create (instead of the current model).
- :rtype: int
- :return: the id of the newly created thread object
- """
- data = {}
- if isinstance(custom_values, dict):
- data = custom_values.copy()
- model = self._context.get('thread_model') or self._name
- RecordModel = self.env[model]
- fields = RecordModel.fields_get()
- if 'name' in fields and not data.get('name'):
- data['name'] = msg_dict.get('subject', '')
- res = RecordModel.create(data)
- return res.id
-
- @api.multi
- def message_update(self, msg_dict, update_vals=None):
- """Called by ``message_process`` when a new message is received
- for an existing thread. The default behavior is to update the record
- with update_vals taken from the incoming email.
- Additional behavior may be implemented by overriding this
- method.
- :param dict msg_dict: a map containing the email details and
- attachments. See ``message_process`` and
- ``mail.message.parse()`` for details.
- :param dict update_vals: a dict containing values to update records
- given their ids; if the dict is None or is
- void, no write operation is performed.
- """
- if update_vals:
- self.write(update_vals)
- return True
-
- def _message_extract_payload(self, message, save_original=False):
- """Extract body as HTML and attachments from the mail message"""
- attachments = []
- body = u''
- if save_original:
- attachments.append(('original_email.eml', message.as_string()))
-
- # Be careful, content-type may contain tricky content like in the
- # following example so test the MIME type with startswith()
- #
- # Content-Type: multipart/related;
- # boundary="_004_3f1e4da175f349248b8d43cdeb9866f1AMSPR06MB343eurprd06pro_";
- # type="text/html"
- if not message.is_multipart() or message.get('content-type', '').startswith("text/"):
- encoding = message.get_content_charset()
- body = message.get_payload(decode=True)
- body = tools.ustr(body, encoding, errors='replace')
- if message.get_content_type() == 'text/plain':
- # text/plain -> <pre/>
- body = tools.append_content_to_html(u'', body, preserve=True)
- else:
- alternative = False
- mixed = False
- html = u''
- for part in message.walk():
- if part.get_content_type() == 'multipart/alternative':
- alternative = True
- if part.get_content_type() == 'multipart/mixed':
- mixed = True
- if part.get_content_maintype() == 'multipart':
- continue # skip container
- # part.get_filename returns decoded value if able to decode, coded otherwise.
- # original get_filename is not able to decode iso-8859-1 (for instance).
- # therefore, iso encoded attachements are not able to be decoded properly with get_filename
- # code here partially copy the original get_filename method, but handle more encoding
- filename=part.get_param('filename', None, 'content-disposition')
- if not filename:
- filename=part.get_param('name', None)
- if filename:
- if isinstance(filename, tuple):
- # RFC2231
- filename=email.utils.collapse_rfc2231_value(filename).strip()
- else:
- filename=decode(filename)
- encoding = part.get_content_charset() # None if attachment
- # 1) Explicit Attachments -> attachments
- if filename or part.get('content-disposition', '').strip().startswith('attachment'):
- attachments.append((filename or 'attachment', part.get_payload(decode=True)))
- continue
- # 2) text/plain -> <pre/>
- if part.get_content_type() == 'text/plain' and (not alternative or not body):
- body = tools.append_content_to_html(body, tools.ustr(part.get_payload(decode=True),
- encoding, errors='replace'), preserve=True)
- # 3) text/html -> raw
- elif part.get_content_type() == 'text/html':
- # mutlipart/alternative have one text and a html part, keep only the second
- # mixed allows several html parts, append html content
- append_content = not alternative or (html and mixed)
- html = tools.ustr(part.get_payload(decode=True), encoding, errors='replace')
- if not append_content:
- body = html
- else:
- body = tools.append_content_to_html(body, html, plaintext=False)
- # 4) Anything else -> attachment
- else:
- attachments.append((filename or 'attachment', part.get_payload(decode=True)))
- return body, attachments
-
- @api.model
- def message_parse(self, message, save_original=False):
- """Parses a string or email.message.Message representing an
- RFC-2822 email, and returns a generic dict holding the
- message details.
-
- :param message: the message to parse
- :type message: email.message.Message | string | unicode
- :param bool save_original: whether the returned dict
- should include an ``original`` attachment containing
- the source of the message
- :rtype: dict
- :return: A dict with the following structure, where each
- field may not be present if missing in original
- message::
-
- { 'message_id': msg_id,
- 'subject': subject,
- 'from': from,
- 'to': to,
- 'cc': cc,
- 'body': unified_body,
- 'attachments': [('file1', 'bytes'),
- ('file2', 'bytes')}
- }
- """
- msg_dict = {
- 'message_type': 'email',
- }
- if not isinstance(message, Message):
- if isinstance(message, unicode):
- # Warning: message_from_string doesn't always work correctly on unicode,
- # we must use utf-8 strings here :-(
- message = message.encode('utf-8')
- message = email.message_from_string(message)
-
- message_id = message['message-id']
- if not message_id:
- # Very unusual situation, be we should be fault-tolerant here
- message_id = "<%s@localhost>" % time.time()
- _logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
- msg_dict['message_id'] = message_id
-
- if message.get('Subject'):
- msg_dict['subject'] = decode(message.get('Subject'))
-
- # Envelope fields not stored in mail.message but made available for message_new()
- msg_dict['from'] = decode(message.get('from'))
- msg_dict['to'] = decode(message.get('to'))
- msg_dict['cc'] = decode(message.get('cc'))
- msg_dict['email_from'] = decode(message.get('from'))
- partner_ids = self._message_find_partners(message, ['To', 'Cc'])
- msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
-
- if message.get('Date'):
- try:
- date_hdr = decode(message.get('Date'))
- parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
- if parsed_date.utcoffset() is None:
- # naive datetime, so we arbitrarily decide to make it
- # UTC, there's no better choice. Should not happen,
- # as RFC2822 requires timezone offset in Date headers.
- stored_date = parsed_date.replace(tzinfo=pytz.utc)
- else:
- stored_date = parsed_date.astimezone(tz=pytz.utc)
- except Exception:
- _logger.info('Failed to parse Date header %r in incoming mail '
- 'with message-id %r, assuming current date/time.',
- message.get('Date'), message_id)
- stored_date = datetime.datetime.now()
- msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
-
- if message.get('In-Reply-To'):
- parent_ids = self.env['mail.message'].search([('message_id', '=', decode(message['In-Reply-To'].strip()))], limit=1)
- if parent_ids:
- msg_dict['parent_id'] = parent_ids.id
-
- if message.get('References') and 'parent_id' not in msg_dict:
- msg_list = mail_header_msgid_re.findall(decode(message['References']))
- parent_ids = self.env['mail.message'].search([('message_id', 'in', [x.strip() for x in msg_list])], limit=1)
- if parent_ids:
- msg_dict['parent_id'] = parent_ids.id
-
- msg_dict['body'], msg_dict['attachments'] = self._message_extract_payload(message, save_original=save_original)
- return msg_dict
-
- #------------------------------------------------------
- # Note specific
- #------------------------------------------------------
-
- @api.multi
- def _message_add_suggested_recipient(self, result, partner=None, email=None, reason=''):
- """ Called by message_get_suggested_recipients, to add a suggested
- recipient in the result dictionary. The form is :
- partner_id, partner_name<partner_email> or partner_name, reason """
- self.ensure_one()
- if email and not partner:
- # get partner info from email
- partner_info = self.message_partner_info_from_emails([email])[0]
- if partner_info.get('partner_id'):
- partner = self.env['res.partner'].sudo().browse([partner_info['partner_id']])[0]
- if email and email in [val[1] for val in result[self.ids[0]]]: # already existing email -> skip
- return result
- if partner and partner in self.message_follower_ids: # recipient already in the followers -> skip
- return result
- if partner and partner.id in [val[0] for val in result[self.ids[0]]]: # already existing partner ID -> skip
- return result
- if partner and partner.email: # complete profile: id, name <email>
- result[self.ids[0]].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
- elif partner: # incomplete profile: id, name
- result[self.ids[0]].append((partner.id, '%s' % (partner.name), reason))
- else: # unknown partner, we are probably managing an email address
- result[self.ids[0]].append((False, email, reason))
- return result
-
- @api.multi
- def message_get_suggested_recipients(self):
- """ Returns suggested recipients for ids. Those are a list of
- tuple (partner_id, partner_name, reason), to be managed by Chatter. """
- result = dict((res_id, []) for res_id in self.ids)
- if 'user_id' in self._fields:
- for obj in self.sudo(): # SUPERUSER because of a read on res.users that would crash otherwise
- if not obj.user_id or not obj.user_id.partner_id:
- continue
- obj._message_add_suggested_recipient(result, partner=obj.user_id.partner_id, reason=self._fields['user_id'].string)
- return result
-
- @api.model
- def _find_partner_from_emails(self, emails, res_model=None, res_id=None, check_followers=True):
- """ Utility method to find partners from email addresses. The rules are :
- 1 - check in document (model | self, id) followers
- 2 - try to find a matching partner that is also an user
- 3 - try to find a matching partner
-
- :param list emails: list of email addresses
- :param string model: model to fetch related record; by default self
- is used.
- :param boolean check_followers: check in document followers
- """
- if res_model is None:
- res_model = self._name
- if res_id is None and self.ids:
- res_id = self.ids[0]
- followers = self.env['res.partner']
- if res_model and res_id:
- record = self.env[res_model].browse(res_id)
- if hasattr(record, 'message_follower_ids'):
- followers = record.message_follower_ids
-
- Partner = self.env['res.partner'].sudo()
- partner_ids = []
-
- for contact in emails:
- partner_id = False
- email_address = tools.email_split(contact)
- if not email_address:
- partner_ids.append(partner_id)
- continue
- email_address = email_address[0]
- # first try: check in document's followers
- for follower in followers:
- if follower.email == email_address:
- partner_id = follower.id
- break
- # second try: check in partners that are also users
- # Escape special SQL characters in email_address to avoid invalid matches
- email_address = (email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_'))
- email_brackets = "<%s>" % email_address
- if not partner_id:
- # exact, case-insensitive match
- partners = Partner.search([('email', '=ilike', email_address), ('user_ids', '!=', False)], limit=1)
- if not partners:
- # if no match with addr-spec, attempt substring match within name-addr pair
- partners = Partner.search([('email', 'ilike', email_brackets), ('user_ids', '!=', False)], limit=1)
- if partners:
- partner_id = partners[0].id
- # third try: check in partners
- if not partner_id:
- # exact, case-insensitive match
- partners = Partner.search([('email', '=ilike', email_address)], limit=1)
- if not partners:
- # if no match with addr-spec, attempt substring match within name-addr pair
- partners = Partner.search([('email', 'ilike', email_brackets)], limit=1)
- if partners:
- partner_id = partners[0].id
- partner_ids.append(partner_id)
- return partner_ids
-
- @api.multi
- def message_partner_info_from_emails(self, emails, link_mail=False):
- """ Convert a list of emails into a list partner_ids and a list
- new_partner_ids. The return value is non conventional because
- it is meant to be used by the mail widget.
-
- :return dict: partner_ids and new_partner_ids """
- self.ensure_one()
- MailMessage = self.env['mail.message'].sudo()
- partner_ids = self._find_partner_from_emails(emails)
- result = list()
- for idx in range(len(emails)):
- email_address = emails[idx]
- partner_id = partner_ids[idx]
- partner_info = {'full_name': email_address, 'partner_id': partner_id}
- result.append(partner_info)
- # link mail with this from mail to the new partner id
- if link_mail and partner_info['partner_id']:
- # Escape special SQL characters in email_address to avoid invalid matches
- email_address = (email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_'))
- email_brackets = "<%s>" % email_address
- MailMessage.search([
- '|',
- ('email_from', '=ilike', email_address),
- ('email_from', 'ilike', email_brackets),
- ('author_id', '=', False)
- ]).write({'author_id': partner_info['partner_id']})
- return result
-
- @api.model
- def _message_preprocess_attachments(self, attachments, attachment_ids, attach_model, attach_res_id):
- """ Preprocess attachments for mail_thread.message_post() or mail_mail.create().
-
- :param list attachments: list of attachment tuples in the form ``(name,content)``,
- where content is NOT base64 encoded
- :param list attachment_ids: a list of attachment ids, not in tomany command form
- :param str attach_model: the model of the attachments parent record
- :param integer attach_res_id: the id of the attachments parent record
- """
- m2m_attachment_ids = []
- if attachment_ids:
- filtered_attachment_ids = self.env['ir.attachment'].sudo().search([
- ('res_model', '=', 'mail.compose.message'),
- ('create_uid', '=', self._uid),
- ('id', 'in', attachment_ids)])
- if filtered_attachment_ids:
- filtered_attachment_ids.write({'res_model': attach_model, 'res_id': attach_res_id})
- m2m_attachment_ids += [(4, id) for id in attachment_ids]
- # Handle attachments parameter, that is a dictionary of attachments
- for name, content in attachments:
- if isinstance(content, unicode):
- content = content.encode('utf-8')
- data_attach = {
- 'name': name,
- 'datas': base64.b64encode(str(content)),
- 'datas_fname': name,
- 'description': name,
- 'res_model': attach_model,
- 'res_id': attach_res_id,
- }
- m2m_attachment_ids.append((0, 0, data_attach))
- return m2m_attachment_ids
-
- @api.multi
- @api.returns('self', lambda value: value.id)
- def message_post(self, body='', subject=None, message_type='notification',
- subtype=None, parent_id=False, attachments=None, context=None,
- content_subtype='html', **kwargs):
- """ Post a new message in an existing thread, returning the new
- mail.message ID.
-
- :param int thread_id: thread ID to post into, or list with one ID;
- if False/0, mail.message model will also be set as False
- :param str body: body of the message, usually raw HTML that will
- be sanitized
- :param str type: see mail_message.type field
- :param str content_subtype:: if plaintext: convert body into html
- :param int parent_id: handle reply to a previous message by adding the
- parent partners to the message in case of private discussion
- :param tuple(str,str) attachments or list id: list of attachment tuples in the form
- ``(name,content)``, where content is NOT base64 encoded
-
- Extra keyword arguments will be used as default column values for the
- new mail.message record. Special cases:
- - attachment_ids: supposed not attached to any document; attach them
- to the related document. Should only be set by Chatter.
- :return int: ID of newly created mail.message
- """
- if attachments is None:
- attachments = {}
- if self.ids and not self.ensure_one():
- raise exceptions.Warning(_('Invalid record set: should be called as model (without records) or on single-record recordset'))
-
- # if we're processing a message directly coming from the gateway, the destination model was
- # set in the context.
- model = False
- if self.ids:
- self.ensure_one()
- model = self._context.get('thread_model', False) if self._name == 'mail.thread' else self._name
- if model and model != self._name and hasattr(self.env[model], 'message_post'):
- RecordModel = self.env[model].with_context(thread_model=None) # TDE: was removing the key ?
- return RecordModel.browse(self.ids).message_post(
- body=body, subject=subject, message_type=message_type,
- subtype=subtype, parent_id=parent_id, attachments=attachments,
- content_subtype=content_subtype, **kwargs)
-
- # 0: Find the message's author, because we need it for private discussion
- author_id = kwargs.get('author_id')
- if author_id is None: # keep False values
- author_id = self.env['mail.message']._get_default_author()
-
- # 1: Handle content subtype: if plaintext, converto into HTML
- if content_subtype == 'plaintext':
- body = tools.plaintext2html(body)
-
- # 2: Private message: add recipients (recipients and author of parent message) - current author
- # + legacy-code management (! we manage only 4 and 6 commands)
- partner_ids = set()
- kwargs_partner_ids = kwargs.pop('partner_ids', [])
- for partner_id in kwargs_partner_ids:
- if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2:
- partner_ids.add(partner_id[1])
- if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3:
- partner_ids |= set(partner_id[2])
- elif isinstance(partner_id, (int, long)):
- partner_ids.add(partner_id)
- else:
- pass # we do not manage anything else
- if parent_id and not model:
- parent_message = self.env['mail.message'].browse(parent_id)
- private_followers = set([partner.id for partner in parent_message.partner_ids])
- if parent_message.author_id:
- private_followers.add(parent_message.author_id.id)
- private_followers -= set([author_id])
- partner_ids |= private_followers
-
- # 3. Attachments
- # - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
- attachment_ids = self._message_preprocess_attachments(
- attachments, kwargs.pop('attachment_ids', []), model, self.ids and self.ids[0] or None)
-
- # 4: mail.message.subtype
- subtype_id = False
- if subtype:
- if '.' not in subtype:
- subtype = 'mail.%s' % subtype
- subtype_id = self.env['ir.model.data'].xmlid_to_res_id(subtype)
-
- # automatically subscribe recipients if asked to
- if self._context.get('mail_post_autofollow') and self.ids and partner_ids:
- partner_to_subscribe = partner_ids
- if self._context.get('mail_post_autofollow_partner_ids'):
- partner_to_subscribe = filter(lambda item: item in self._context.get('mail_post_autofollow_partner_ids'), partner_ids)
- self.message_subscribe(list(partner_to_subscribe))
-
- # _mail_flat_thread: automatically set free messages to the first posted message
- MailMessage = self.env['mail.message']
- if self._mail_flat_thread and model and not parent_id and self.ids:
- messages = MailMessage.search(['&', ('res_id', '=', self.ids[0]), ('model', '=', model), ('message_type', '=', 'email')], order="id ASC", limit=1)
- if not messages:
- messages = MailMessage.search(['&', ('res_id', '=', self.ids[0]), ('model', '=', model)], order="id ASC", limit=1)
- parent_id = messages and messages[0].id or False
- # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
- elif parent_id:
- messages = MailMessage.sudo().search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1)
- # avoid loops when finding ancestors
- processed_list = []
- if messages:
- message = messages[0]
- while (message.parent_id and message.parent_id.id not in processed_list):
- processed_list.append(message.parent_id.id)
- message = message.parent_id
- parent_id = message.id
-
- values = kwargs
- values.update({
- 'author_id': author_id,
- 'model': model,
- 'res_id': model and self.ids[0] or False,
- 'body': body,
- 'subject': subject or False,
- 'message_type': message_type,
- 'parent_id': parent_id,
- 'attachment_ids': attachment_ids,
- 'subtype_id': subtype_id,
- 'partner_ids': [(4, pid) for pid in partner_ids],
- })
-
- # Avoid warnings about non-existing fields
- for x in ('from', 'to', 'cc'):
- values.pop(x, None)
-
- # Post the message
- new_message = MailMessage.create(values)
-
- # Post-process: subscribe author, update message_last_post
- if model and model != 'mail.thread' and self.ids and subtype_id:
- # done with SUPERUSER_ID, because on some models users can post only with read access, not necessarily write access
- self.sudo().write({'message_last_post': fields.datetime.now()})
- if new_message.author_id and model and self.ids and message_type != 'notification' and not self._context.get('mail_create_nosubscribe'):
- self.message_subscribe([new_message.author_id.id])
- return new_message
-
- # ------------------------------------------------------
- # Followers API
- # ------------------------------------------------------
-
- @api.multi
- def message_get_subscription_data(self, user_pid=None):
- # TDE CLEANME: is that method usefull ?
- """ Wrapper to get subtypes data. """
- return self._get_subscription_data(user_pid=user_pid)
-
- @api.multi
- def message_subscribe_users(self, user_ids=None, subtype_ids=None):
- """ Wrapper on message_subscribe, using users. If user_ids is not
- provided, subscribe uid instead. """
- if user_ids is None:
- user_ids = [self._uid]
- result = self.message_subscribe(self.env['res.users'].browse(user_ids).mapped('partner_id').ids, subtype_ids=subtype_ids)
- if user_ids and result:
- self.pool['ir.ui.menu'].clear_cache()
- return result
-
- @api.multi
- def message_subscribe(self, partner_ids, subtype_ids=None, context=None):
- """ Add partners to the records followers. """
- # not necessary for computation, but saves an access right check
- if not partner_ids:
- return True
-
- Followers = self.env['mail.followers']
- Subtype = self.env['mail.message.subtype']
-
- if set(partner_ids) == set([self.env.user.partner_id.id]):
- try:
- self.check_access_rights('read')
- self.check_access_rule('read')
- except exceptions.AccessError:
- return False
- else:
- self.check_access_rights('write')
- self.check_access_rule('write')
-
- existing_pids_dict = dict.fromkeys(self.ids, self.env['res.partner'])
- followers = Followers.sudo().search([
- '&', '&',
- ('res_model', '=', self._name),
- ('res_id', 'in', self.ids),
- ('partner_id', 'in', partner_ids)])
- for follower in followers:
- existing_pids_dict[follower.res_id] |= follower.partner_id
-
- # subtype_ids specified: update already subscribed partners
- if subtype_ids and followers:
- followers.write({'subtype_ids': [(6, 0, subtype_ids)]})
- # subtype_ids not specified: do not update already subscribed partner, fetch default subtypes for new partners
- if subtype_ids is None:
- subtype_ids = Subtype.search([
- ('default', '=', True),
- '|',
- ('res_model', '=', self._name),
- ('res_model', '=', False)]).ids
-
- for record in self:
- existing_pids = existing_pids_dict[record.id]
- new_partners = self.env['res.partner'].browse(partner_ids) - existing_pids
-
- # subscribe new followers
- for new_partner in new_partners:
- Followers.sudo().create({
- 'res_model': self._name,
- 'res_id': record.id,
- 'partner_id': new_partner.id,
- 'subtype_ids': [(6, 0, subtype_ids)]})
-
- return True
-
- @api.multi
- def message_unsubscribe_users(self, user_ids=None):
- """ Wrapper on message_subscribe, using users. If user_ids is not
- provided, unsubscribe uid instead. """
- if user_ids is None:
- user_ids = [self._uid]
- partner_ids = [user.partner_id.id for user in self.env['res.users'].browse(user_ids)]
- result = self.message_unsubscribe(partner_ids)
- if partner_ids and result:
- self.pool['ir.ui.menu'].clear_cache()
- return result
-
- @api.multi
- def message_unsubscribe(self, partner_ids):
- """ Remove partners from the records followers. """
- # not necessary for computation, but saves an access right check
- if not partner_ids:
- return True
- user_pid = self.env.user.partner_id.id
- if set(partner_ids) == set([user_pid]):
- self.check_access_rights('read')
- self.check_access_rule('read')
- else:
- self.check_access_rights('write')
- self.check_access_rule('write')
- self.env['mail.followers'].sudo().search([
- ('res_model', '=', self._name),
- ('res_id', 'in', self.ids),
- ('partner_id', 'in', partner_ids)
- ]).unlink()
-
- @api.model
- def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None):
- """ Returns the list of relational fields linking to res.users that should
- trigger an auto subscribe. The default list checks for the fields
- - called 'user_id'
- - linking to res.users
- - with track_visibility set
- In OpenERP V7, this is sufficent for all major addon such as opportunity,
- project, issue, recruitment, sale.
- Override this method if a custom behavior is needed about fields
- that automatically subscribe users.
- """
- if auto_follow_fields is None:
- auto_follow_fields = ['user_id']
- user_field_lst = []
- for name, field in self._fields.items():
- if name in auto_follow_fields and name in updated_fields and getattr(field, 'track_visibility', False) and field.comodel_name == 'res.users':
- user_field_lst.append(name)
- return user_field_lst
-
- @api.multi
- def _message_auto_subscribe_notify(self, partner_ids):
- """ Send notifications to the partners automatically subscribed to the thread
- Override this method if a custom behavior is needed about partners
- that should be notified or messages that should be sent
- """
- # find first email message, set it as unread for auto_subscribe fields for them to have a notification
- if not partner_ids:
- return
- for record_id in self.ids:
- # TDE FIXME: is sudo necessary / a goot option ?
- MailMessage = self.env['mail.message'].sudo()
- messages = MailMessage.search([
- ('model', '=', self._name),
- ('res_id', '=', record_id),
- ('message_type', '=', 'email')], limit=1)
- if not messages:
- messages = MailMessage.search([
- ('model', '=', self._name),
- ('res_id', '=', record_id)], limit=1)
- if messages:
- message = messages[0]
- notification_obj = self.env['mail.notification']
- partners = self.env['res.partner'].sudo().browse(partner_ids)
- notification_obj._notify(message, recipients=partners)
- if message.parent_id:
- partner_ids_to_parent_notify = set(partner_ids).difference(partner.id for partner in message.parent_id.notified_partner_ids)
- for partner_id in partner_ids_to_parent_notify:
- notification_obj.create({
- 'message_id': message.parent_id.id,
- 'partner_id': partner_id,
- 'is_read': True,
- })
-
- @api.multi
- def message_auto_subscribe(self, updated_fields, values=None):
- """ Handle auto subscription. Two methods for auto subscription exist:
-
- - tracked res.users relational fields, such as user_id fields. Those fields
- must be relation fields toward a res.users record, and must have the
- track_visilibity attribute set.
- - using subtypes parent relationship: check if the current model being
- modified has an header record (such as a project for tasks) whose followers
- can be added as followers of the current records. Example of structure
- with project and task:
-
- - st_project_1.parent_id = st_task_1
- - st_project_1.res_model = 'project.project'
- - st_project_1.relation_field = 'project_id'
- - st_task_1.model = 'project.task'
-
- :param list updated_fields: list of updated fields to track
- :param dict values: updated values; if None, the first record will be browsed
- to get the values. Added after releasing 7.0, therefore
- not merged with updated_fields argumment.
- """
- new_followers = dict()
-
- # fetch auto_follow_fields: res.users relation fields whose changes are tracked for subscription
- user_field_lst = self._message_get_auto_subscribe_fields(updated_fields)
-
- # fetch header subtypes
- subtypes = self.env['mail.message.subtype'].search(['|', ('res_model', '=', False), ('parent_id.res_model', '=', self._name)])
-
- # if no change in tracked field or no change in tracked relational field: quit
- relation_fields = set([subtype.relation_field for subtype in subtypes if subtype.relation_field is not False])
- if not any(relation in updated_fields for relation in relation_fields) and not user_field_lst:
- return True
-
- # legacy behavior: if values is not given, compute the values by browsing
- # @TDENOTE: remove me in 8.0
- if values is None:
- record = self[0]
- for updated_field in updated_fields:
- field_value = getattr(record, updated_field)
- if isinstance(field_value, models.BaseModel):
- field_value = field_value.id
- values[updated_field] = field_value
-
- # find followers of headers, update structure for new followers
- headers = set()
- for subtype in subtypes:
- if subtype.relation_field and values.get(subtype.relation_field):
- headers.add((subtype.res_model, values.get(subtype.relation_field)))
- if headers:
- header_domain = ['|'] * (len(headers) - 1)
- for header in headers:
- header_domain += ['&', ('res_model', '=', header[0]), ('res_id', '=', header[1])]
- for header_follower in self.env['mail.followers'].sudo().search(header_domain):
- for subtype in header_follower.subtype_ids:
- if subtype.parent_id and subtype.parent_id.res_model == self._name:
- new_followers.setdefault(header_follower.partner_id.id, set()).add(subtype.parent_id.id)
- elif subtype.res_model is False:
- new_followers.setdefault(header_follower.partner_id.id, set()).add(subtype.id)
-
- # add followers coming from res.users relational fields that are tracked
- user_ids = [values[name] for name in user_field_lst if values.get(name)]
- user_pids = [user.partner_id.id for user in self.env['res.users'].sudo().browse(user_ids)]
- for partner_id in user_pids:
- new_followers.setdefault(partner_id, None)
-
- for pid, subtypes in new_followers.items():
- subtypes = list(subtypes) if subtypes is not None else None
- self.message_subscribe([pid], subtypes)
-
- self._message_auto_subscribe_notify(user_pids)
-
- return True
-
- # ------------------------------------------------------
- # Thread management
- # ------------------------------------------------------
-
- @api.multi
- def message_mark_as_unread(self):
- """ Set as unread. """
- partner_id = self.env.user.partner_id.id
- self._cr.execute('''
- UPDATE mail_notification SET
- is_read=false
- WHERE
- message_id IN (SELECT id from mail_message where res_id=any(%s) and model=%s limit 1) and
- partner_id = %s
- ''', (self.ids, self._name, partner_id))
- self.env['mail.notification'].invalidate_cache(['is_read'])
- return True
-
- @api.multi
- def message_mark_as_read(self):
- """ Set as read. """
- partner_id = self.env.user.partner_id.id
- self._cr.execute('''
- UPDATE mail_notification SET
- is_read=true
- WHERE
- message_id IN (SELECT id FROM mail_message WHERE res_id=ANY(%s) AND model=%s) AND
- partner_id = %s
- ''', (self.ids, self._name, partner_id))
- self.env['mail.notification'].invalidate_cache(['is_read'])
- return True
-
- @api.model
- def get_suggested_thread(self, removed_suggested_threads=None):
- """Return a list of suggested threads, sorted by the numbers of followers"""
- # TDE HACK: originally by MAT from portal/mail_mail.py but not working until the inheritance graph bug is not solved in trunk
- # TDE FIXME: relocate in portal when it won't be necessary to reload the hr.employee model in an additional bridge module
- if 'is_portal' in self.pool['res.groups']._fields:
- if any(group.is_portal for group in self.env.user.sudo().groups_id):
- return []
-
- threads = []
- if removed_suggested_threads is None:
- removed_suggested_threads = []
-
- for thread in self.search([('id', 'not in', removed_suggested_threads), ('message_is_follower', '=', False)]):
- data = {
- 'id': thread.id,
- 'popularity': len(thread.message_follower_ids),
- 'name': thread.name,
- 'image_small': thread.image_small
- }
- threads.append(data)
- return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]
-
- @api.multi
- def message_change_thread(self, new_thread):
- """
- Transfer the list of the mail thread messages from an model to another
-
- :param id : the old res_id of the mail.message
- :param new_res_id : the new res_id of the mail.message
- :param new_model : the name of the new model of the mail.message
-
- Example : my_lead.message_change_thread(my_project_issue)
- will transfer the context of the thread of my_lead to my_project_issue
- """
- self.ensuer_one()
- # get the sbtype id of the comment Message
- subtype_res_id = self.env.ref('mail.mt_comment')
-
- # get the ids of the comment and not-comment of the thread
- # TDE check: sudo on mail.message, to be sure all messages are moved ?
- MailMessage = self.env['mail.message']
- msg_comment = MailMessage.search([
- ('model', '=', self._name),
- ('res_id', '=', id),
- ('subtype_id', '=', subtype_res_id)])
- msg_not_comment = MailMessage.search([
- ('model', '=', self._name),
- ('res_id', '=', id),
- ('subtype_id', '!=', subtype_res_id)])
-
- # update the messages
- msg_comment.write({"res_id": new_thread.id, "model": new_thread._name})
- msg_not_comment.write({"res_id": new_thread.id, "model": new_thread._name, "subtype_id": None})
- return True