2011-07-22 16:34:57 +00:00
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
2012-03-13 15:06:35 +00:00
# Copyright (C) 2009-today OpenERP SA (<http://www.openerp.com>)
2011-07-22 16:34:57 +00:00
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
2011-08-23 17:58:09 +00:00
import base64
2012-12-18 02:11:23 +00:00
import datetime
2012-08-22 08:38:13 +00:00
import dateutil
2011-07-22 16:34:57 +00:00
import email
2013-11-25 13:04:55 +00:00
try :
import simplejson as json
except ImportError :
import json
from lxml import etree
2011-07-22 16:34:57 +00:00
import logging
2012-08-23 18:54:43 +00:00
import pytz
import time
2011-07-22 16:34:57 +00:00
import xmlrpclib
2012-08-09 17:16:55 +00:00
from email . message import Message
2012-11-21 09:58:31 +00:00
2012-12-20 12:17:44 +00:00
from openerp import tools
2012-09-14 11:58:53 +00:00
from openerp import SUPERUSER_ID
2012-12-19 12:13:46 +00:00
from openerp . addons . mail . mail_message import decode
2013-04-26 13:28:47 +00:00
from openerp . osv import fields , osv , orm
2013-11-14 11:32:31 +00:00
from openerp . osv . orm import browse_record , browse_null
2012-12-06 14:56:32 +00:00
from openerp . tools . safe_eval import safe_eval as eval
2013-02-06 09:44:14 +00:00
from openerp . tools . translate import _
2012-08-23 18:54:43 +00:00
2012-02-01 16:21:36 +00:00
_logger = logging . getLogger ( __name__ )
2011-07-22 16:34:57 +00:00
2012-10-15 13:34:38 +00:00
2012-08-10 13:19:19 +00:00
def decode_header ( message , header , separator = ' ' ) :
2013-01-07 11:13:51 +00:00
return separator . join ( map ( decode , filter ( None , message . get_all ( header , [ ] ) ) ) )
2012-08-10 13:19:19 +00:00
2012-09-20 10:17:04 +00:00
2012-09-04 12:18:38 +00:00
class mail_thread ( osv . AbstractModel ) :
2012-08-31 08:01:03 +00:00
''' 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 .
2012-10-15 13:23:13 +00:00
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 .
2011-07-22 16:34:57 +00:00
'''
_name = ' mail.thread '
_description = ' Email Thread '
2012-10-15 13:23:13 +00:00
_mail_flat_thread = True
2013-07-23 14:45:07 +00:00
_mail_post_access = ' write '
2011-07-22 16:34:57 +00:00
2012-12-19 09:38:44 +00:00
# Automatic logging system if mail installed
# _track = {
# 'field': {
2012-12-20 12:17:44 +00:00
# 'module.subtype_xml': lambda self, cr, uid, obj, context=None: obj[state] == done,
# 'module.subtype_xml2': lambda self, cr, uid, obj, context=None: obj[state] != done,
2012-12-19 09:38:44 +00:00
# },
# 'field2': {
# ...
# },
# }
# where
# :param string field: field name
# :param module.subtype_xml: xml_id of a mail.message.subtype (i.e. mail.mt_comment)
# :param obj: is a browse_record
# :param function lambda: returns whether the tracking should record using this subtype
2012-12-19 00:04:02 +00:00
_track = { }
2011-07-22 16:34:57 +00:00
2013-03-21 13:31:39 +00:00
def get_empty_list_help ( self , cr , uid , help , context = None ) :
2013-03-29 10:01:51 +00:00
""" Override of BaseModel.get_empty_list_help() to generate an help message
that adds alias information . """
model = context . get ( ' empty_list_help_model ' )
res_id = context . get ( ' empty_list_help_id ' )
2013-04-05 11:58:30 +00:00
ir_config_parameter = self . pool . get ( " ir.config_parameter " )
catchall_domain = ir_config_parameter . get_param ( cr , uid , " mail.catchall.domain " , context = context )
2013-03-29 10:01:51 +00:00
document_name = context . get ( ' empty_list_help_document_name ' , _ ( ' document ' ) )
alias = None
2013-04-05 11:58:30 +00:00
if catchall_domain and model and res_id : # specific res_id -> find its alias (i.e. section_id specified)
2013-03-29 10:01:51 +00:00
object_id = self . pool . get ( model ) . browse ( cr , uid , res_id , context = context )
2013-05-30 12:41:28 +00:00
# check that the alias effectively creates new records
2013-06-12 12:34:29 +00:00
if object_id . alias_id and object_id . alias_id . alias_name and \
object_id . alias_id . alias_model_id and \
2013-05-30 12:41:28 +00:00
object_id . alias_id . alias_model_id . model == self . _name and \
object_id . alias_id . alias_force_thread_id == 0 :
2013-05-07 13:01:49 +00:00
alias = object_id . alias_id
2013-07-04 08:36:31 +00:00
if catchall_domain and model and not alias : #check for example alias if res_id not given or given res_id dose not contain alias-> generic help message, take an example alias (i.e. alias of some section_id)
2013-03-29 10:01:51 +00:00
alias_obj = self . pool . get ( ' mail.alias ' )
2014-01-26 09:32:36 +00:00
alias_ids = alias_obj . search ( cr , uid , [ ( " alias_parent_model_id.model " , " = " , model ) , ( " alias_name " , " != " , False ) , ( ' alias_force_thread_id ' , ' = ' , False ) ] , context = context , order = ' id ASC ' )
if alias_ids and len ( alias_ids ) == 1 :
2013-03-29 10:01:51 +00:00
alias = alias_obj . browse ( cr , uid , alias_ids [ 0 ] , context = context )
if alias :
alias_email = alias . name_get ( ) [ 0 ] [ 1 ]
return _ ( """ <p class= ' oe_view_nocontent_create ' >
2013-05-07 13:01:49 +00:00
Click here to add new % ( document ) s or send an email to : < a href = ' mailto: %(email)s ' > % ( email ) s < / a >
2013-03-29 10:01:51 +00:00
< / p >
% ( static_help ) s """
) % {
' document ' : document_name ,
' email ' : alias_email ,
' static_help ' : help or ' '
}
if document_name != ' document ' and help and help . find ( " oe_view_nocontent_create " ) == - 1 :
2013-05-07 13:01:49 +00:00
return _ ( " <p class= ' oe_view_nocontent_create ' >Click here to add new %(document)s </p> %(static_help)s " ) % {
2013-03-29 10:01:51 +00:00
' document ' : document_name ,
' static_help ' : help or ' ' ,
}
2013-03-20 13:41:16 +00:00
2013-02-06 09:44:14 +00:00
return help
2012-08-15 13:36:43 +00:00
def _get_message_data ( self , cr , uid , ids , name , args , context = None ) :
2012-09-20 10:17:04 +00:00
""" Computes:
- message_unread : has uid unread message for the document
- message_summary : html snippet summarizing the Chatter for kanban views """
2013-02-26 15:07:07 +00:00
res = dict ( ( id , dict ( message_unread = False , message_unread_count = 0 , message_summary = ' ' ) ) for id in ids )
2012-11-12 13:10:51 +00:00
user_pid = self . pool . get ( ' res.users ' ) . read ( cr , uid , uid , [ ' partner_id ' ] , context = context ) [ ' partner_id ' ] [ 0 ]
2012-08-22 11:03:13 +00:00
2012-11-12 13:10:51 +00:00
# search for unread messages, directly in SQL to improve performances
cr . execute ( """ SELECT m.res_id FROM mail_message m
RIGHT JOIN mail_notification n
2012-11-16 09:50:03 +00:00
ON ( n . message_id = m . id AND n . partner_id = % s AND ( n . read = False or n . read IS NULL ) )
2012-11-12 13:10:51 +00:00
WHERE m . model = % s AND m . res_id in % s """ ,
( user_pid , self . _name , tuple ( ids ) , ) )
2013-02-14 14:34:17 +00:00
for result in cr . fetchall ( ) :
res [ result [ 0 ] ] [ ' message_unread ' ] = True
2013-02-26 15:07:07 +00:00
res [ result [ 0 ] ] [ ' message_unread_count ' ] + = 1
2012-09-20 10:17:04 +00:00
2013-02-26 15:07:07 +00:00
for id in ids :
if res [ id ] [ ' message_unread_count ' ] :
title = res [ id ] [ ' message_unread_count ' ] > 1 and _ ( " You have %d unread messages " ) % res [ id ] [ ' message_unread_count ' ] or _ ( " You have one unread message " )
res [ id ] [ ' message_summary ' ] = " <span class= ' oe_kanban_mail_new ' title= ' %s ' ><span class= ' oe_e ' >9</span> %d %s </span> " % ( title , res [ id ] . pop ( ' message_unread_count ' ) , _ ( " New " ) )
2012-09-21 09:50:37 +00:00
return res
2012-10-15 13:23:13 +00:00
2013-07-15 08:01:06 +00:00
def read_followers_data ( self , cr , uid , follower_ids , context = None ) :
2013-07-23 09:55:09 +00:00
result = [ ]
2014-01-23 10:34:25 +00:00
technical_group = self . pool . get ( ' ir.model.data ' ) . get_object ( cr , uid , ' base ' , ' group_no_one ' , context = context )
2013-07-15 11:37:36 +00:00
for follower in self . pool . get ( ' res.partner ' ) . browse ( cr , uid , follower_ids , context = context ) :
2013-07-23 09:55:09 +00:00
is_editable = uid in map ( lambda x : x . id , technical_group . users )
is_uid = uid in map ( lambda x : x . id , follower . user_ids )
data = ( follower . id ,
follower . name ,
{ ' is_editable ' : is_editable , ' is_uid ' : is_uid } ,
)
result . append ( data )
return result
2013-04-11 12:35:37 +00:00
2013-06-05 13:10:31 +00:00
def _get_subscription_data ( self , cr , uid , ids , name , args , user_pid = None , context = None ) :
2012-09-20 10:17:04 +00:00
""" Computes:
- message_subtype_data : data about document subtypes : which are
available , which are followed if any """
2012-10-16 11:17:53 +00:00
res = dict ( ( id , dict ( message_subtype_data = ' ' ) ) for id in ids )
2013-06-05 13:10:31 +00:00
if user_pid is None :
user_pid = self . pool . get ( ' res.users ' ) . read ( cr , uid , uid , [ ' partner_id ' ] , context = context ) [ ' partner_id ' ] [ 0 ]
2012-09-20 10:17:04 +00:00
# find current model subtypes, add them to a dictionary
subtype_obj = self . pool . get ( ' mail.message.subtype ' )
subtype_ids = subtype_obj . search ( cr , uid , [ ' | ' , ( ' res_model ' , ' = ' , self . _name ) , ( ' res_model ' , ' = ' , False ) ] , context = context )
2012-09-20 11:49:47 +00:00
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 ) )
for id in ids :
res [ id ] [ ' message_subtype_data ' ] = subtype_dict . copy ( )
2012-09-20 10:17:04 +00:00
# find the document followers, update the data
fol_obj = self . pool . get ( ' mail.followers ' )
2012-10-19 09:59:19 +00:00
fol_ids = fol_obj . search ( cr , uid , [
2012-09-20 10:17:04 +00:00
( ' partner_id ' , ' = ' , user_pid ) ,
( ' res_id ' , ' in ' , ids ) ,
( ' res_model ' , ' = ' , self . _name ) ,
] , context = context )
2012-10-19 09:59:19 +00:00
for fol in fol_obj . browse ( cr , uid , fol_ids , context = context ) :
2012-09-20 11:49:47 +00:00
thread_subtype_dict = res [ fol . res_id ] [ ' message_subtype_data ' ]
2012-09-20 10:17:04 +00:00
for subtype in fol . subtype_ids :
thread_subtype_dict [ subtype . name ] [ ' followed ' ] = True
2012-09-20 11:49:47 +00:00
res [ fol . res_id ] [ ' message_subtype_data ' ] = thread_subtype_dict
2012-10-15 13:23:13 +00:00
2012-02-03 11:21:16 +00:00
return res
2012-04-25 05:41:43 +00:00
2012-11-27 14:32:22 +00:00
def _search_message_unread ( self , cr , uid , obj = None , name = None , domain = None , context = None ) :
2012-12-05 14:05:39 +00:00
return [ ( ' message_ids.to_read ' , ' = ' , True ) ]
2012-06-14 10:09:22 +00:00
2012-10-08 15:12:34 +00:00
def _get_followers ( self , cr , uid , ids , name , arg , context = None ) :
fol_obj = self . pool . get ( ' mail.followers ' )
fol_ids = fol_obj . search ( cr , SUPERUSER_ID , [ ( ' res_model ' , ' = ' , self . _name ) , ( ' res_id ' , ' in ' , ids ) ] )
2012-10-16 11:17:53 +00:00
res = dict ( ( id , dict ( message_follower_ids = [ ] , message_is_follower = False ) ) for id in ids )
user_pid = self . pool . get ( ' res.users ' ) . read ( cr , uid , uid , [ ' partner_id ' ] , context = context ) [ ' partner_id ' ] [ 0 ]
2012-10-08 15:12:34 +00:00
for fol in fol_obj . browse ( cr , SUPERUSER_ID , fol_ids ) :
2012-10-16 11:17:53 +00:00
res [ fol . res_id ] [ ' message_follower_ids ' ] . append ( fol . partner_id . id )
if fol . partner_id . id == user_pid :
res [ fol . res_id ] [ ' message_is_follower ' ] = True
2012-10-08 15:12:34 +00:00
return res
def _set_followers ( self , cr , uid , id , name , value , arg , context = None ) :
2012-10-10 07:25:10 +00:00
if not value :
return
2012-10-08 15:12:34 +00:00
partner_obj = self . pool . get ( ' res.partner ' )
fol_obj = self . pool . get ( ' mail.followers ' )
# read the old set of followers, and determine the new set of followers
fol_ids = fol_obj . search ( cr , SUPERUSER_ID , [ ( ' res_model ' , ' = ' , self . _name ) , ( ' res_id ' , ' = ' , id ) ] )
old = set ( fol . partner_id . id for fol in fol_obj . browse ( cr , SUPERUSER_ID , fol_ids ) )
new = set ( old )
2012-11-02 08:27:07 +00:00
for command in value or [ ] :
2012-10-08 15:12:34 +00:00
if isinstance ( command , ( int , long ) ) :
new . add ( command )
elif command [ 0 ] == 0 :
new . add ( partner_obj . create ( cr , uid , command [ 2 ] , context = context ) )
elif command [ 0 ] == 1 :
partner_obj . write ( cr , uid , [ command [ 1 ] ] , command [ 2 ] , context = context )
new . add ( command [ 1 ] )
elif command [ 0 ] == 2 :
partner_obj . unlink ( cr , uid , [ command [ 1 ] ] , context = context )
new . discard ( command [ 1 ] )
elif command [ 0 ] == 3 :
new . discard ( command [ 1 ] )
elif command [ 0 ] == 4 :
new . add ( command [ 1 ] )
elif command [ 0 ] == 5 :
new . clear ( )
elif command [ 0 ] == 6 :
new = set ( command [ 2 ] )
# remove partners that are no longer followers
2014-01-10 10:01:33 +00:00
self . message_unsubscribe ( cr , uid , [ id ] , list ( old - new ) , context = context )
2012-10-08 15:12:34 +00:00
# add new followers
2014-01-10 10:01:33 +00:00
self . message_subscribe ( cr , uid , [ id ] , list ( new - old ) , context = context )
2012-10-08 15:12:34 +00:00
def _search_followers ( self , cr , uid , obj , name , args , context ) :
2013-05-02 12:39:45 +00:00
""" Search function for message_follower_ids
Do not use with operator ' not in ' . Use instead message_is_followers
"""
2012-10-08 15:12:34 +00:00
fol_obj = self . pool . get ( ' mail.followers ' )
res = [ ]
for field , operator , value in args :
assert field == name
2013-05-02 12:39:45 +00:00
# TOFIX make it work with not in
assert operator != " not in " , " Do not search message_follower_ids with ' not in ' "
2012-10-08 15:12:34 +00:00
fol_ids = fol_obj . search ( cr , SUPERUSER_ID , [ ( ' res_model ' , ' = ' , self . _name ) , ( ' partner_id ' , operator , value ) ] )
res_ids = [ fol . res_id for fol in fol_obj . browse ( cr , SUPERUSER_ID , fol_ids ) ]
res . append ( ( ' id ' , ' in ' , res_ids ) )
return res
2013-05-02 12:39:45 +00:00
def _search_is_follower ( self , cr , uid , obj , name , args , context ) :
""" Search function for message_is_follower """
res = [ ]
for field , operator , value in args :
assert field == name
partner_id = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . partner_id . id
if ( operator == ' = ' and value ) or ( operator == ' != ' and not value ) : # is a follower
res_ids = self . search ( cr , uid , [ ( ' message_follower_ids ' , ' in ' , [ partner_id ] ) ] , context = context )
else : # is not a follower or unknown domain
mail_ids = self . search ( cr , uid , [ ( ' message_follower_ids ' , ' in ' , [ partner_id ] ) ] , context = context )
res_ids = self . search ( cr , uid , [ ( ' id ' , ' not in ' , mail_ids ) ] , context = context )
res . append ( ( ' id ' , ' in ' , res_ids ) )
return res
2011-07-22 16:34:57 +00:00
_columns = {
2013-05-02 12:39:45 +00:00
' message_is_follower ' : fields . function ( _get_followers , type = ' boolean ' ,
fnct_search = _search_is_follower , string = ' Is a Follower ' , multi = ' _get_followers, ' ) ,
2012-10-08 15:12:34 +00:00
' message_follower_ids ' : fields . function ( _get_followers , fnct_inv = _set_followers ,
2014-01-06 10:00:10 +00:00
fnct_search = _search_followers , type = ' many2many ' , priority = - 10 ,
2013-05-02 12:39:45 +00:00
obj = ' res.partner ' , string = ' Followers ' , multi = ' _get_followers ' ) ,
2012-08-15 13:36:43 +00:00
' message_ids ' : fields . one2many ( ' mail.message ' , ' res_id ' ,
2012-09-05 15:51:21 +00:00
domain = lambda self : [ ( ' model ' , ' = ' , self . _name ) ] ,
2012-12-05 15:36:09 +00:00
auto_join = True ,
2012-09-05 15:51:21 +00:00
string = ' Messages ' ,
2012-09-04 13:36:48 +00:00
help = " Messages and communication history " ) ,
2012-11-27 14:32:22 +00:00
' message_unread ' : fields . function ( _get_message_data ,
fnct_search = _search_message_unread , multi = " _get_message_data " ,
type = ' boolean ' , string = ' Unread Messages ' ,
2012-09-04 13:36:48 +00:00
help = " If checked new messages require your attention. " ) ,
2012-08-15 13:36:43 +00:00
' message_summary ' : fields . function ( _get_message_data , method = True ,
type = ' text ' , string = ' Summary ' , multi = " _get_message_data " ,
2012-06-21 15:23:11 +00:00
help = " Holds the Chatter summary (number of messages, ...). " \
" This summary is directly in html format in order to " \
" be inserted in kanban views. " ) ,
2011-07-22 16:34:57 +00:00
}
2013-11-25 13:04:55 +00:00
def _get_user_chatter_options ( self , cr , uid , context = None ) :
options = {
' display_log_button ' : False
}
group_ids = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . groups_id
group_user_id = self . pool . get ( " ir.model.data " ) . get_object_reference ( cr , uid , ' base ' , ' group_user ' ) [ 1 ]
is_employee = group_user_id in [ group . id for group in group_ids ]
if is_employee :
options [ ' display_log_button ' ] = True
return options
def fields_view_get ( self , cr , uid , view_id = None , view_type = ' form ' , context = None , toolbar = False , submenu = False ) :
res = super ( mail_thread , 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 ' , ' {} ' ) )
options . update ( self . _get_user_chatter_options ( cr , uid , context = context ) )
node . set ( ' options ' , json . dumps ( options ) )
res [ ' arch ' ] = etree . tostring ( doc )
return res
2012-02-28 14:06:32 +00:00
#------------------------------------------------------
2012-12-18 12:25:58 +00:00
# CRUD overrides for automatic subscription and logging
2012-02-28 14:06:32 +00:00
#------------------------------------------------------
2012-04-25 05:41:43 +00:00
2012-12-18 12:25:58 +00:00
def create ( self , cr , uid , values , context = None ) :
2012-12-19 11:05:02 +00:00
""" Chatter override :
- subscribe uid
- subscribe followers of parent
- log a creation message
"""
2012-12-12 12:55:18 +00:00
if context is None :
context = { }
2013-11-26 17:17:52 +00:00
2014-01-06 10:00:10 +00:00
# subscribe uid unless asked not to
if not context . get ( ' mail_create_nosubscribe ' ) :
pid = self . pool [ ' res.users ' ] . browse ( cr , SUPERUSER_ID , uid ) . partner_id . id
message_follower_ids = values . get ( ' message_follower_ids ' ) or [ ] # webclient can send None or False
message_follower_ids . append ( [ 4 , pid ] )
values [ ' message_follower_ids ' ] = message_follower_ids
2014-01-10 10:01:33 +00:00
# add operation to ignore access rule checking for subscription
context_operation = dict ( context , operation = ' create ' )
else :
context_operation = context
thread_id = super ( mail_thread , self ) . create ( cr , uid , values , context = context_operation )
2012-12-18 12:25:58 +00:00
2013-05-29 13:14:58 +00:00
# automatic logging unless asked not to (mainly for various testing purpose)
if not context . get ( ' mail_create_nolog ' ) :
self . message_post ( cr , uid , thread_id , body = _ ( ' %s created ' ) % ( self . _description ) , context = context )
2013-06-19 12:38:29 +00:00
# auto_subscribe: take values and defaults into account
2013-11-14 11:32:31 +00:00
create_values = dict ( values )
2013-06-19 12:38:29 +00:00
for key , val in context . iteritems ( ) :
if key . startswith ( ' default_ ' ) :
2013-11-14 11:32:31 +00:00
create_values [ key [ 8 : ] ] = val
self . message_auto_subscribe ( cr , uid , [ thread_id ] , create_values . keys ( ) , context = context , values = create_values )
2012-12-18 12:25:58 +00:00
2013-05-29 13:14:58 +00:00
# track values
2013-11-25 16:38:08 +00:00
track_ctx = dict ( context )
if ' lang ' not in track_ctx :
track_ctx [ ' lang ' ] = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . lang
tracked_fields = self . _get_tracked_fields ( cr , uid , values . keys ( ) , context = track_ctx )
2013-05-29 13:14:58 +00:00
if tracked_fields :
initial_values = { thread_id : dict ( ( item , False ) for item in tracked_fields ) }
2013-11-25 16:38:08 +00:00
self . message_track ( cr , uid , [ thread_id ] , tracked_fields , initial_values , context = track_ctx )
2012-06-04 09:33:24 +00:00
return thread_id
2012-04-25 05:41:43 +00:00
2012-12-18 12:25:58 +00:00
def write ( self , cr , uid , ids , values , context = None ) :
2013-11-25 16:38:08 +00:00
if context is None :
context = { }
2012-12-19 12:31:42 +00:00
if isinstance ( ids , ( int , long ) ) :
ids = [ ids ]
2012-12-19 16:42:39 +00:00
# Track initial values of tracked fields
2013-11-25 16:38:08 +00:00
track_ctx = dict ( context )
if ' lang ' not in track_ctx :
track_ctx [ ' lang ' ] = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . lang
tracked_fields = self . _get_tracked_fields ( cr , uid , values . keys ( ) , context = track_ctx )
2012-12-18 23:49:07 +00:00
if tracked_fields :
2013-11-27 11:00:46 +00:00
records = self . browse ( cr , uid , ids , context = track_ctx )
2013-06-27 14:46:47 +00:00
initial_values = dict ( ( this . id , dict ( ( key , getattr ( this , key ) ) for key in tracked_fields . keys ( ) ) ) for this in records )
2012-12-19 16:42:39 +00:00
# Perform write, update followers
2012-12-18 12:25:58 +00:00
result = super ( mail_thread , self ) . write ( cr , uid , ids , values , context = context )
2013-11-14 11:32:31 +00:00
self . message_auto_subscribe ( cr , uid , ids , values . keys ( ) , context = context , values = values )
2012-12-19 16:42:39 +00:00
# Perform the tracking
2012-12-18 23:49:07 +00:00
if tracked_fields :
2013-11-25 16:38:08 +00:00
self . message_track ( cr , uid , ids , tracked_fields , initial_values , context = track_ctx )
2012-12-18 12:25:58 +00:00
return result
2012-03-13 15:06:35 +00:00
def unlink ( self , cr , uid , ids , context = None ) :
2012-08-22 11:03:13 +00:00
""" Override unlink to delete messages and followers. This cannot be
cascaded , because link is done through ( res_model , res_id ) . """
2012-03-13 15:06:35 +00:00
msg_obj = self . pool . get ( ' mail.message ' )
2012-08-22 11:03:13 +00:00
fol_obj = self . pool . get ( ' mail.followers ' )
2012-04-20 12:42:00 +00:00
# delete messages and notifications
2012-08-22 11:03:13 +00:00
msg_ids = msg_obj . search ( cr , uid , [ ( ' model ' , ' = ' , self . _name ) , ( ' res_id ' , ' in ' , ids ) ] , context = context )
msg_obj . unlink ( cr , uid , msg_ids , context = context )
2012-12-05 15:08:27 +00:00
# delete
res = super ( mail_thread , self ) . unlink ( cr , uid , ids , context = context )
2012-08-22 11:03:13 +00:00
# delete followers
2012-12-12 10:41:41 +00:00
fol_ids = fol_obj . search ( cr , SUPERUSER_ID , [ ( ' res_model ' , ' = ' , self . _name ) , ( ' res_id ' , ' in ' , ids ) ] , context = context )
2012-12-05 15:08:27 +00:00
fol_obj . unlink ( cr , SUPERUSER_ID , fol_ids , context = context )
return res
2012-09-14 13:52:45 +00:00
def copy ( self , cr , uid , id , default = None , context = None ) :
default = default or { }
default [ ' message_ids ' ] = [ ]
default [ ' message_follower_ids ' ] = [ ]
return super ( mail_thread , self ) . copy ( cr , uid , id , default = default , context = context )
2012-04-25 05:41:43 +00:00
2012-11-21 09:58:31 +00:00
#------------------------------------------------------
# Automatically log tracked fields
#------------------------------------------------------
2012-12-18 12:25:58 +00:00
def _get_tracked_fields ( self , cr , uid , updated_fields , context = None ) :
""" Return a structure of tracked fields for the current model.
: param list updated_fields : modified field names
: return list : a list of ( field_name , column_info obj ) , containing
always tracked fields and modified on_change fields
"""
2012-12-18 23:49:07 +00:00
lst = [ ]
for name , column_info in self . _all_columns . items ( ) :
2012-12-19 11:05:02 +00:00
visibility = getattr ( column_info . column , ' track_visibility ' , False )
2012-12-20 12:17:44 +00:00
if visibility == ' always ' or ( visibility == ' onchange ' and name in updated_fields ) or name in self . _track :
2012-12-18 23:49:07 +00:00
lst . append ( name )
2012-12-19 11:05:02 +00:00
if not lst :
return lst
2012-12-18 23:49:07 +00:00
return self . fields_get ( cr , uid , lst , context = context )
2012-12-19 00:42:22 +00:00
def message_track ( self , cr , uid , ids , tracked_fields , initial_values , context = None ) :
2012-12-19 11:05:02 +00:00
2012-12-27 11:34:05 +00:00
def convert_for_display ( value , col_info ) :
if not value and col_info [ ' type ' ] == ' boolean ' :
return ' False '
2012-12-18 12:25:58 +00:00
if not value :
2012-12-18 23:49:07 +00:00
return ' '
2012-12-27 11:34:05 +00:00
if col_info [ ' type ' ] == ' many2one ' :
2013-06-27 14:46:47 +00:00
return value . name_get ( ) [ 0 ] [ 1 ]
2012-12-27 11:34:05 +00:00
if col_info [ ' type ' ] == ' selection ' :
return dict ( col_info [ ' selection ' ] ) [ value ]
2012-12-18 12:25:58 +00:00
return value
2012-12-19 16:42:39 +00:00
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> • <b> %s </b>: ' % change . get ( ' col_info ' )
if change . get ( ' old_value ' ) :
message + = ' %s → ' % change . get ( ' old_value ' )
message + = ' %s </div> ' % change . get ( ' new_value ' )
return message
2012-12-19 11:05:02 +00:00
if not tracked_fields :
return True
2013-06-27 14:46:47 +00:00
for browse_record in self . browse ( cr , uid , ids , context = context ) :
initial = initial_values [ browse_record . id ]
changes = set ( )
2012-12-18 12:25:58 +00:00
tracked_values = { }
2012-12-19 11:05:02 +00:00
2012-12-18 12:25:58 +00:00
# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name , col_info in tracked_fields . items ( ) :
2013-06-27 14:46:47 +00:00
initial_value = initial [ col_name ]
record_value = getattr ( browse_record , col_name )
if record_value == initial_value and getattr ( self . _all_columns [ col_name ] . column , ' track_visibility ' , None ) == ' always ' :
2012-12-18 23:49:07 +00:00
tracked_values [ col_name ] = dict ( col_info = col_info [ ' string ' ] ,
2013-06-27 14:46:47 +00:00
new_value = convert_for_display ( record_value , col_info ) )
2013-06-27 14:55:19 +00:00
elif record_value != initial_value and ( record_value or initial_value ) : # because browse null != False
2012-12-20 11:47:30 +00:00
if getattr ( self . _all_columns [ col_name ] . column , ' track_visibility ' , None ) in [ ' always ' , ' onchange ' ] :
2012-12-19 11:32:05 +00:00
tracked_values [ col_name ] = dict ( col_info = col_info [ ' string ' ] ,
2013-06-27 14:46:47 +00:00
old_value = convert_for_display ( initial_value , col_info ) ,
new_value = convert_for_display ( record_value , col_info ) )
2012-12-19 11:32:05 +00:00
if col_name in tracked_fields :
2013-06-27 14:46:47 +00:00
changes . add ( col_name )
2012-12-19 00:42:22 +00:00
if not changes :
2012-12-18 12:25:58 +00:00
continue
# find subtypes and post messages or log if no subtype found
2012-12-19 00:42:22 +00:00
subtypes = [ ]
for field , track_info in self . _track . items ( ) :
2012-12-19 11:05:02 +00:00
if field not in changes :
continue
2012-12-19 00:42:22 +00:00
for subtype , method in track_info . items ( ) :
2013-06-27 14:46:47 +00:00
if method ( self , cr , uid , browse_record , context ) :
2012-12-19 00:42:22 +00:00
subtypes . append ( subtype )
2012-12-18 23:49:07 +00:00
posted = False
2012-12-18 12:25:58 +00:00
for subtype in subtypes :
2014-01-29 03:08:53 +00:00
subtype_rec = self . pool . get ( ' ir.model.data ' ) . xmlid_to_object ( cr , uid , subtype , context = context )
2014-01-23 10:34:25 +00:00
if not ( subtype_rec and subtype_rec . exists ( ) ) :
2013-12-06 10:11:17 +00:00
_logger . debug ( ' subtype %s not found ' % subtype )
2012-12-18 12:25:58 +00:00
continue
2012-12-19 21:23:30 +00:00
message = format_message ( subtype_rec . description if subtype_rec . description else subtype_rec . name , tracked_values )
2013-06-27 14:46:47 +00:00
self . message_post ( cr , uid , browse_record . id , body = message , subtype = subtype , context = context )
2012-12-18 23:49:07 +00:00
posted = True
if not posted :
2012-12-19 16:42:39 +00:00
message = format_message ( ' ' , tracked_values )
2013-06-27 14:46:47 +00:00
self . message_post ( cr , uid , browse_record . id , body = message , context = context )
2012-12-18 12:25:58 +00:00
return True
2012-11-21 09:58:31 +00:00
2012-02-01 16:21:36 +00:00
#------------------------------------------------------
2012-06-21 09:37:55 +00:00
# mail.message wrappers and tools
2012-02-01 16:21:36 +00:00
#------------------------------------------------------
2012-04-25 05:41:43 +00:00
2012-08-31 17:15:07 +00:00
def _needaction_domain_get ( self , cr , uid , context = None ) :
2012-08-15 13:36:43 +00:00
if self . _needaction :
2012-08-28 09:53:23 +00:00
return [ ( ' message_unread ' , ' = ' , True ) ]
2012-08-15 13:36:43 +00:00
return [ ]
2012-08-31 17:15:07 +00:00
2013-03-07 13:25:17 +00:00
def _garbage_collect_attachments ( self , cr , uid , context = None ) :
""" 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 )
ir_attachment_obj = self . pool . get ( ' ir.attachment ' )
attach_ids = ir_attachment_obj . search ( cr , uid , [
( ' res_model ' , ' = ' , ' mail.compose.message ' ) ,
( ' res_id ' , ' = ' , 0 ) ,
( ' create_date ' , ' < ' , limit_date_str ) ,
( ' write_date ' , ' < ' , limit_date_str ) ,
] , context = context )
ir_attachment_obj . unlink ( cr , uid , attach_ids , context = context )
return True
2013-05-28 14:44:47 +00:00
def check_mail_message_access ( self , cr , uid , mids , operation , model_obj = None , context = 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 not model_obj :
model_obj = self
2013-07-23 14:45:07 +00:00
if hasattr ( self , ' _mail_post_access ' ) :
create_allow = self . _mail_post_access
2013-05-28 14:44:47 +00:00
else :
2013-07-23 14:45:07 +00:00
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
model_obj . check_access_rights ( cr , uid , check_operation )
model_obj . check_access_rule ( cr , uid , mids , check_operation , context = context )
2013-05-28 14:44:47 +00:00
2013-04-26 14:40:19 +00:00
def _get_formview_action ( self , cr , uid , id , model = None , context = None ) :
2013-04-26 13:28:47 +00:00
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific view ids for example .
2013-04-16 13:28:13 +00:00
2013-04-26 13:28:47 +00:00
: param int id : id of the document to open
: param string model : specific model that overrides self . _name
2013-04-16 13:28:13 +00:00
"""
2013-04-26 13:28:47 +00:00
return {
' type ' : ' ir.actions.act_window ' ,
' res_model ' : model or self . _name ,
' view_type ' : ' form ' ,
' view_mode ' : ' form ' ,
' views ' : [ ( False , ' form ' ) ] ,
' target ' : ' current ' ,
' res_id ' : id ,
}
2013-04-16 13:28:13 +00:00
2013-04-26 13:28:47 +00:00
def _get_inbox_action_xml_id ( self , cr , uid , context = None ) :
2013-04-17 12:22:25 +00:00
""" 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 ' )
2013-04-16 13:28:13 +00:00
2013-04-17 12:22:25 +00:00
def message_redirect_action ( self , cr , uid , context = None ) :
""" 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
2013-04-16 13:28:13 +00:00
2013-04-17 12:22:25 +00:00
"""
if context is None :
context = { }
2013-04-16 13:28:13 +00:00
2013-04-17 12:22:25 +00:00
# default action is the Inbox action
self . pool . get ( ' res.users ' ) . browse ( cr , SUPERUSER_ID , uid , context = context )
2013-04-26 13:28:47 +00:00
act_model , act_id = self . pool . get ( ' ir.model.data ' ) . get_object_reference ( cr , uid , * self . _get_inbox_action_xml_id ( cr , uid , context = context ) )
2013-04-17 12:22:25 +00:00
action = self . pool . get ( act_model ) . read ( cr , uid , act_id , [ ] )
2013-10-18 14:49:24 +00:00
params = 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 ' )
if not msg_id and not ( model and res_id ) :
2013-04-17 12:22:25 +00:00
return action
2013-10-18 14:49:24 +00:00
if msg_id and not ( model and res_id ) :
msg = self . pool . get ( ' mail.message ' ) . browse ( cr , uid , msg_id , context = context )
2013-10-28 16:01:00 +00:00
if msg . exists ( ) :
model , res_id = msg . model , msg . res_id
2013-10-18 14:49:24 +00:00
# if model + res_id found: try to redirect to the document or fallback on the Inbox
if model and res_id :
model_obj = self . pool . get ( model )
if model_obj . check_access_rights ( cr , uid , ' read ' , raise_exception = False ) :
2013-04-29 09:33:29 +00:00
try :
2013-10-18 14:49:24 +00:00
model_obj . check_access_rule ( cr , uid , [ res_id ] , ' read ' , context = context )
2013-04-29 09:33:29 +00:00
if not hasattr ( model_obj , ' _get_formview_action ' ) :
2013-10-18 14:49:24 +00:00
action = self . pool . get ( ' mail.thread ' ) . _get_formview_action ( cr , uid , res_id , model = model , context = context )
2013-04-29 09:33:29 +00:00
else :
2013-10-18 14:49:24 +00:00
action = model_obj . _get_formview_action ( cr , uid , res_id , context = context )
2013-04-29 09:33:29 +00:00
except ( osv . except_osv , orm . except_orm ) :
pass
2013-10-18 14:49:24 +00:00
action . update ( {
' context ' : {
' search_default_model ' : model ,
' search_default_res_id ' : res_id ,
}
} )
2013-04-17 12:22:25 +00:00
return action
2013-04-16 13:28:13 +00:00
2013-01-02 13:00:25 +00:00
#------------------------------------------------------
# Email specific
#------------------------------------------------------
def message_get_reply_to ( self , cr , uid , ids , context = None ) :
2013-04-16 13:28:13 +00:00
""" Returns the preferred reply-to email address that is basically
the alias of the document , if it exists . """
2013-01-02 13:00:25 +00:00
if not self . _inherits . get ( ' mail.alias ' ) :
2013-01-02 13:50:20 +00:00
return [ False for id in ids ]
2013-01-02 13:00:25 +00:00
return [ " %s @ %s " % ( record [ ' alias_name ' ] , record [ ' alias_domain ' ] )
if record . get ( ' alias_domain ' ) and record . get ( ' alias_name ' )
else False
2013-04-17 08:59:27 +00:00
for record in self . read ( cr , SUPERUSER_ID , ids , [ ' alias_name ' , ' alias_domain ' ] , context = context ) ]
2013-01-02 13:00:25 +00:00
2012-08-20 07:42:42 +00:00
#------------------------------------------------------
# Mail gateway
#------------------------------------------------------
2011-12-09 14:28:39 +00:00
def message_capable_models ( self , cr , uid , context = None ) :
2012-09-04 14:50:11 +00:00
""" Used by the plugin addon, based for plugin_outlook and others. """
2011-12-09 14:28:39 +00:00
ret_dict = { }
for model_name in self . pool . obj_list ( ) :
2013-03-29 14:37:20 +00:00
model = self . pool [ model_name ]
2013-06-11 13:33:14 +00:00
if hasattr ( model , " message_process " ) and hasattr ( model , " message_post " ) :
2012-09-04 14:50:11 +00:00
ret_dict [ model_name ] = model . _description
2011-12-09 14:28:39 +00:00
return ret_dict
2012-08-22 08:38:13 +00:00
def _message_find_partners ( self , cr , uid , message , header_fields = [ ' From ' ] , context = None ) :
2013-04-11 10:17:20 +00:00
""" Find partners related to some header fields of the message.
2013-04-16 13:28:13 +00:00
: param string message : an email . message instance """
2012-08-22 08:38:13 +00:00
s = ' , ' . join ( [ decode ( message . get ( h ) ) for h in header_fields if message . get ( h ) ] )
2013-04-16 13:28:13 +00:00
return filter ( lambda x : x , self . _find_partner_from_emails ( cr , uid , None , tools . email_split ( s ) , context = context ) )
2012-06-14 14:17:32 +00:00
2013-06-24 15:18:27 +00:00
def message_route_verify ( self , cr , uid , message , message_dict , route , update_author = True , assert_model = True , create_fallback = True , context = None ) :
""" 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 ]
model_pool = None
def _create_bounce_email ( ) :
mail_mail = self . pool . get ( ' mail.mail ' )
mail_id = mail_mail . create ( cr , uid , {
' 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 ,
} , context = context )
mail_mail . send ( cr , uid , [ mail_id ] , context = context )
def _warn ( message ) :
_logger . warning ( ' Routing mail with Message-Id %s : route %s : %s ' ,
message_id , route , message )
# Wrong model
if model and not model in self . pool :
if assert_model :
assert model in self . pool , ' Routing: unknown target model %s ' % model
_warn ( ' unknown target model %s ' % model )
return ( )
elif model :
model_pool = self . pool [ model ]
# Private message: should not contain any thread_id
if not model and thread_id :
if assert_model :
2013-11-28 13:32:26 +00:00
assert thread_id == 0 , ' 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 )
2013-06-24 15:18:27 +00:00
thread_id = 0
2013-08-07 14:24:18 +00:00
# Private message: should have a parent_id (only answers)
if not model and not message_dict . get ( ' parent_id ' ) :
if assert_model :
assert message_dict . get ( ' parent_id ' ) , ' 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 ( )
2013-06-24 15:18:27 +00:00
# Existing Document: check if exists; if not, fallback on create if allowed
if thread_id and not model_pool . exists ( cr , uid , thread_id ) :
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 :
assert model_pool . exists ( cr , uid , thread_id ) , ' 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
2013-08-07 14:24:18 +00:00
if thread_id and model and not hasattr ( model_pool , ' message_update ' ) :
2013-06-24 15:18:27 +00:00
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 ( model_pool , ' 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
2013-08-07 14:24:18 +00:00
if not thread_id and model and not hasattr ( model_pool , ' message_new ' ) :
2013-06-24 15:18:27 +00:00
if assert_model :
assert hasattr ( model_pool , ' message_new ' ) , ' 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 . _find_partner_from_emails ( cr , uid , thread_id , [ email_from ] , model = model , context = context )
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 = self . pool [ model ] . browse ( cr , uid , thread_id , context = context )
else :
obj = self . pool [ alias . alias_parent_model_id . model ] . browse ( cr , uid , alias . alias_parent_thread_id , context = context )
if not author_id or not author_id 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 ( )
return ( model , thread_id , route [ 2 ] , route [ 3 ] , route [ 4 ] )
2013-04-16 13:28:13 +00:00
def message_route ( self , cr , uid , message , message_dict , model = None , thread_id = None ,
2012-08-07 18:04:12 +00:00
custom_values = None , context = None ) :
""" Attempt to figure out the correct target model, thread_id,
custom_values and user_id to use for an incoming message .
2012-08-10 13:19:19 +00:00
Multiple values may be returned , if a message had multiple
recipients matching existing mail . aliases , for example .
2012-08-07 18:04:12 +00:00
2012-08-15 13:36:43 +00:00
The following heuristics are used , in this order :
2012-08-07 18:04:12 +00:00
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
2012-08-15 13:36:43 +00:00
custom_value ( not needed as no creation will take place )
2012-08-07 18:04:12 +00:00
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
2013-06-24 15:18:27 +00:00
: param dict message_dict : dictionary holding message variables
2012-08-07 18:04:12 +00:00
: 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 .
2013-06-24 15:18:27 +00:00
: return : list of [ model , thread_id , custom_values , user_id , alias ]
2011-07-22 16:34:57 +00:00
"""
2012-08-09 17:16:55 +00:00
assert isinstance ( message , Message ) , ' message must be an email.message.Message at this point '
2013-11-28 13:32:26 +00:00
mail_msg_obj = self . pool [ ' mail.message ' ]
2013-04-16 13:28:13 +00:00
fallback_model = model
# Get email.message.Message variables for future processing
2012-08-09 17:16:55 +00:00
message_id = message . get ( ' Message-Id ' )
2013-03-22 13:44:10 +00:00
email_from = decode_header ( message , ' From ' )
email_to = decode_header ( message , ' To ' )
2012-11-08 15:25:02 +00:00
references = decode_header ( message , ' References ' )
in_reply_to = decode_header ( message , ' In-Reply-To ' )
2013-06-25 09:26:06 +00:00
thread_references = references or in_reply_to
2013-11-28 10:39:21 +00:00
# 1. message is a reply to an existing message (exact match of message_id)
msg_references = thread_references . split ( )
2013-12-05 09:39:21 +00:00
mail_message_ids = mail_msg_obj . search ( cr , uid , [ ( ' message_id ' , ' in ' , msg_references ) ] , context = context )
if mail_message_ids :
original_msg = mail_msg_obj . browse ( cr , SUPERUSER_ID , mail_message_ids [ 0 ] , context = context )
model , thread_id = original_msg . model , original_msg . res_id
_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 , uid )
route = self . message_route_verify (
cr , uid , message , message_dict ,
( model , thread_id , custom_values , uid , None ) ,
update_author = True , assert_model = True , create_fallback = True , context = context )
return route and [ route ] or [ ]
2012-08-09 17:16:55 +00:00
2013-11-28 13:32:26 +00:00
# 2. message is a reply to an existign thread (6.1 compatibility)
2012-11-08 15:25:02 +00:00
ref_match = thread_references and tools . reference_re . search ( thread_references )
2012-08-07 18:04:12 +00:00
if ref_match :
thread_id = int ( ref_match . group ( 1 ) )
2013-04-16 13:28:13 +00:00
model = ref_match . group ( 2 ) or fallback_model
2013-03-29 14:37:20 +00:00
if thread_id and model in self . pool :
model_obj = self . pool [ model ]
2013-11-28 13:32:26 +00:00
compat_mail_msg_ids = mail_msg_obj . search (
cr , uid , [
( ' message_id ' , ' = ' , False ) ,
( ' model ' , ' = ' , model ) ,
( ' res_id ' , ' = ' , thread_id ) ,
] , context = context )
if compat_mail_msg_ids and model_obj . exists ( cr , uid , thread_id ) and hasattr ( model_obj , ' message_update ' ) :
_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 , uid )
route = self . message_route_verify (
cr , uid , message , message_dict ,
( model , thread_id , custom_values , uid , None ) ,
update_author = True , assert_model = True , create_fallback = True , context = context )
2013-06-24 15:18:27 +00:00
return route and [ route ] or [ ]
2012-08-15 13:36:43 +00:00
2013-04-16 13:28:13 +00:00
# 2. Reply to a private message
2013-06-24 15:18:27 +00:00
if in_reply_to :
2013-11-28 13:32:26 +00:00
mail_message_ids = mail_msg_obj . search ( cr , uid , [
2013-03-12 13:30:29 +00:00
( ' message_id ' , ' = ' , in_reply_to ) ,
' ! ' , ( ' message_id ' , ' ilike ' , ' reply_to ' )
] , limit = 1 , context = context )
2013-08-07 14:24:18 +00:00
if mail_message_ids :
2013-11-28 13:32:26 +00:00
mail_message = mail_msg_obj . browse ( cr , uid , mail_message_ids [ 0 ] , context = context )
2013-03-22 13:44:10 +00:00
_logger . info ( ' Routing mail from %s to %s with Message-Id %s : direct reply to a private message: %s , custom_values: %s , uid: %s ' ,
2013-08-07 14:24:18 +00:00
email_from , email_to , message_id , mail_message . id , custom_values , uid )
2013-06-24 15:18:27 +00:00
route = self . message_route_verify ( cr , uid , message , message_dict ,
2013-08-07 14:24:18 +00:00
( mail_message . model , mail_message . res_id , custom_values , uid , None ) ,
2013-06-24 15:18:27 +00:00
update_author = True , assert_model = True , create_fallback = True , context = context )
return route and [ route ] or [ ]
2012-11-08 15:25:02 +00:00
2013-04-16 13:28:13 +00:00
# 3. Look for a matching mail.alias entry
2013-06-24 15:18:27 +00:00
# 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.
2013-06-24 16:24:57 +00:00
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 ' ) ] )
2012-08-16 16:43:11 +00:00
local_parts = [ e . split ( ' @ ' ) [ 0 ] for e in tools . email_split ( rcpt_tos ) ]
2013-06-24 15:18:27 +00:00
if local_parts :
2012-08-07 18:04:12 +00:00
mail_alias = self . pool . get ( ' mail.alias ' )
alias_ids = mail_alias . search ( cr , uid , [ ( ' alias_name ' , ' in ' , local_parts ) ] )
2013-06-24 15:18:27 +00:00
if alias_ids :
routes = [ ]
for alias in mail_alias . browse ( cr , uid , alias_ids , context = context ) :
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 = 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 )
_logger . info ( ' Routing mail from %s to %s with Message-Id %s : direct alias match: %r ' ,
email_from , email_to , message_id , route )
route = self . message_route_verify ( cr , uid , message , message_dict , route ,
update_author = True , assert_model = True , create_fallback = True , context = context )
if route :
routes . append ( route )
return routes
2013-04-16 13:28:13 +00:00
# 4. Fallback to the provided parameters, if they work
2013-06-24 15:18:27 +00:00
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
_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 , uid )
route = self . message_route_verify ( cr , uid , message , message_dict ,
( fallback_model , thread_id , custom_values , uid , None ) ,
update_author = True , assert_model = True , context = context )
if route :
return [ route ]
2013-04-16 13:28:13 +00:00
# AssertionError if no routes found and if no bounce occured
2013-06-24 15:18:27 +00:00
assert False , \
2013-06-24 16:24:57 +00:00
" 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 )
2011-07-22 16:34:57 +00:00
2013-09-16 11:47:06 +00:00
def message_route_process ( self , cr , uid , message , message_dict , routes , context = None ) :
# postpone setting message_dict.partner_ids after message_post, to avoid double notifications
partner_ids = message_dict . pop ( ' partner_ids ' , [ ] )
2013-09-13 11:54:08 +00:00
thread_id = False
for model , thread_id , custom_values , user_id , alias in routes :
if self . _name == ' mail.thread ' :
context . update ( { ' thread_model ' : model } )
if model :
model_pool = self . pool [ model ]
assert thread_id and hasattr ( model_pool , ' message_update ' ) or hasattr ( model_pool , ' message_new ' ) , \
" Undeliverable mail with Message-Id %s , model %s does not accept incoming emails " % \
2013-09-16 11:47:06 +00:00
( message_dict [ ' message_id ' ] , model )
2013-09-13 11:54:08 +00:00
# disabled subscriptions during message_new/update to avoid having the system user running the
# email gateway become a follower of all inbound messages
nosub_ctx = dict ( context , mail_create_nosubscribe = True , mail_create_nolog = True )
if thread_id and hasattr ( model_pool , ' message_update ' ) :
2013-09-16 11:47:06 +00:00
model_pool . message_update ( cr , user_id , [ thread_id ] , message_dict , context = nosub_ctx )
2013-09-13 11:54:08 +00:00
else :
2013-09-16 11:47:06 +00:00
thread_id = model_pool . message_new ( cr , user_id , message_dict , custom_values , context = nosub_ctx )
2013-09-13 11:54:08 +00:00
else :
assert thread_id == 0 , " Posting a message without model should be with a null res_id, to create a private message. "
model_pool = self . pool . get ( ' mail.thread ' )
if not hasattr ( model_pool , ' message_post ' ) :
context [ ' thread_model ' ] = model
model_pool = self . pool [ ' mail.thread ' ]
2013-09-16 11:47:06 +00:00
new_msg_id = model_pool . message_post ( cr , uid , [ thread_id ] , context = context , subtype = ' mail.mt_comment ' , * * message_dict )
2013-09-13 11:54:08 +00:00
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
self . pool . get ( ' mail.message ' ) . write ( cr , uid , [ new_msg_id ] , { ' partner_ids ' : partner_ids } , context = context )
return thread_id
2011-09-07 15:13:48 +00:00
def message_process ( self , cr , uid , model , message , custom_values = None ,
2011-09-08 00:16:51 +00:00
save_original = False , strip_attachments = False ,
2012-06-14 14:17:32 +00:00
thread_id = None , context = None ) :
2012-11-08 15:25:02 +00:00
""" Process an incoming RFC2822 email message, relying on
` ` mail . message . parse ( ) ` ` for the parsing operation ,
and ` ` message_route ( ) ` ` to figure out the target model .
2012-08-15 13:36:43 +00:00
2012-11-08 15:25:02 +00:00
Once the target model is known , its ` ` message_new ` ` method
is called with the new message ( if the thread record did not exist )
2012-08-20 07:26:03 +00:00
or its ` ` message_update ` ` method ( if it did ) .
2012-08-07 18:04:12 +00:00
2012-11-08 15:25:02 +00:00
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 .
2012-08-07 18:04:12 +00:00
: 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
2011-07-22 16:34:57 +00:00
: type message : string or xmlrpclib . Binary
2011-09-13 13:23:40 +00:00
: type dict custom_values : optional dictionary of field values
2012-08-07 18:04:12 +00:00
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 )
2011-09-07 15:13:48 +00:00
: param bool save_original : whether to keep a copy of the original
2012-07-03 12:20:20 +00:00
email source attached to the message after it is imported .
2011-09-08 00:16:51 +00:00
: param bool strip_attachments : whether to strip all attachments
2012-07-03 12:20:20 +00:00
before processing the message , in order to save some space .
2012-06-14 14:17:32 +00:00
: 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 .
2011-07-22 16:34:57 +00:00
"""
2012-10-15 13:34:38 +00:00
if context is None :
context = { }
2012-08-07 18:04:12 +00:00
2011-07-22 16:34:57 +00:00
# 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 )
2013-03-22 12:48:09 +00:00
# parse the message, verify we are not in a loop by checking message_id is not duplicated
2012-08-23 18:54:43 +00:00
msg = self . message_parse ( cr , uid , msg_txt , save_original = save_original , context = context )
2012-10-15 13:34:38 +00:00
if strip_attachments :
msg . pop ( ' attachments ' , None )
2013-09-13 11:54:08 +00:00
2013-03-22 12:48:09 +00:00
if msg . get ( ' message_id ' ) : # should always be True as message_parse generate one if missing
existing_msg_ids = self . pool . get ( ' mail.message ' ) . search ( cr , SUPERUSER_ID , [
( ' message_id ' , ' = ' , msg . get ( ' message_id ' ) ) ,
] , context = context )
if existing_msg_ids :
2013-04-16 13:28:13 +00:00
_logger . info ( ' Ignored mail from %s to %s with Message-Id %s : found duplicated Message-Id during processing ' ,
2013-03-22 13:44:10 +00:00
msg . get ( ' from ' ) , msg . get ( ' to ' ) , msg . get ( ' message_id ' ) )
2013-03-22 12:48:09 +00:00
return False
# find possible routes for the message
2013-04-16 13:28:13 +00:00
routes = self . message_route ( cr , uid , msg_txt , msg , model , thread_id , custom_values , context = context )
2013-09-16 11:47:06 +00:00
thread_id = self . message_route_process ( cr , uid , msg_txt , msg , routes , context = context )
2012-09-13 07:17:24 +00:00
return thread_id
2011-07-22 16:34:57 +00:00
def message_new ( self , cr , uid , msg_dict , custom_values = None , context = None ) :
2011-08-23 17:58:09 +00:00
""" Called by ``message_process`` when a new message is received
2012-04-25 05:41:43 +00:00
for a given thread model , if the message did not belong to
2011-08-25 12:27:57 +00:00
an existing thread .
The default behavior is to create a new record of the corresponding
2012-09-04 14:50:11 +00:00
model ( based on some very basic info extracted from the message ) .
2011-07-22 16:34:57 +00:00
Additional behavior may be implemented by overriding this method .
: param dict msg_dict : a map containing the email details and
2011-08-23 17:58:09 +00:00
attachments . See ` ` message_process ` ` and
2011-08-25 12:27:57 +00:00
` ` mail . message . parse ` ` for details .
2011-07-22 16:34:57 +00:00
: 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
"""
if context is None :
context = { }
2013-01-08 14:20:21 +00:00
data = { }
if isinstance ( custom_values , dict ) :
data = custom_values . copy ( )
2011-07-22 16:34:57 +00:00
model = context . get ( ' thread_model ' ) or self . _name
2013-03-29 14:37:20 +00:00
model_pool = self . pool [ model ]
2011-07-22 16:34:57 +00:00
fields = model_pool . fields_get ( cr , uid , context = context )
if ' name ' in fields and not data . get ( ' name ' ) :
2012-08-14 08:04:21 +00:00
data [ ' name ' ] = msg_dict . get ( ' subject ' , ' ' )
2011-07-22 16:34:57 +00:00
res_id = model_pool . create ( cr , uid , data , context = context )
return res_id
2012-06-04 14:12:54 +00:00
def message_update ( self , cr , uid , ids , msg_dict , update_vals = None , context = None ) :
2011-08-23 17:58:09 +00:00
""" Called by ``message_process`` when a new message is received
2012-09-04 14:50:11 +00:00
for an existing thread . The default behavior is to update the record
with update_vals taken from the incoming email .
2011-07-22 16:34:57 +00:00
Additional behavior may be implemented by overriding this
method .
: param dict msg_dict : a map containing the email details and
2012-07-05 10:22:19 +00:00
attachments . See ` ` message_process ` ` and
` ` mail . message . parse ( ) ` ` for details .
: param dict update_vals : a dict containing values to update records
2012-06-04 14:12:54 +00:00
given their ids ; if the dict is None or is
void , no write operation is performed .
2011-07-22 16:34:57 +00:00
"""
2012-06-04 14:12:54 +00:00
if update_vals :
self . write ( cr , uid , ids , update_vals , context = context )
2011-07-22 16:34:57 +00:00
return True
2012-08-23 18:54:43 +00:00
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 ( ) ) )
if not message . is_multipart ( ) or ' text/ ' in message . get ( ' content-type ' , ' ' ) :
encoding = message . get_content_charset ( )
body = message . get_payload ( decode = True )
body = tools . ustr ( body , encoding , errors = ' replace ' )
2012-09-05 16:01:45 +00:00
if message . get_content_type ( ) == ' text/plain ' :
# text/plain -> <pre/>
2012-11-14 11:11:29 +00:00
body = tools . append_content_to_html ( u ' ' , body , preserve = True )
2012-08-23 18:54:43 +00:00
else :
2013-08-23 12:06:11 +00:00
alternative = False
2012-08-23 18:54:43 +00:00
for part in message . walk ( ) :
2013-08-23 12:06:11 +00:00
if part . get_content_type ( ) == ' multipart/alternative ' :
alternative = True
2012-08-23 18:54:43 +00:00
if part . get_content_maintype ( ) == ' multipart ' :
2012-12-19 11:05:02 +00:00
continue # skip container
2014-01-02 16:11:49 +00:00
# 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 )
2012-12-19 11:05:02 +00:00
encoding = part . get_content_charset ( ) # None if attachment
2012-08-23 18:54:43 +00:00
# 1) Explicit Attachments -> attachments
2012-09-05 15:51:21 +00:00
if filename or part . get ( ' content-disposition ' , ' ' ) . strip ( ) . startswith ( ' attachment ' ) :
2014-01-02 16:11:49 +00:00
attachments . append ( ( filename or ' attachment ' , part . get_payload ( decode = True ) ) )
2012-08-23 18:54:43 +00:00
continue
# 2) text/plain -> <pre/>
if part . get_content_type ( ) == ' text/plain ' and ( not alternative or not body ) :
2012-08-31 15:51:03 +00:00
body = tools . append_content_to_html ( body , tools . ustr ( part . get_payload ( decode = True ) ,
2012-11-14 11:11:29 +00:00
encoding , errors = ' replace ' ) , preserve = True )
2012-08-23 18:54:43 +00:00
# 3) text/html -> raw
elif part . get_content_type ( ) == ' text/html ' :
html = tools . ustr ( part . get_payload ( decode = True ) , encoding , errors = ' replace ' )
if alternative :
body = html
2011-09-07 15:13:48 +00:00
else :
2012-08-31 15:51:03 +00:00
body = tools . append_content_to_html ( body , html , plaintext = False )
2012-08-23 18:54:43 +00:00
# 4) Anything else -> attachment
else :
attachments . append ( ( filename or ' attachment ' , part . get_payload ( decode = True ) ) )
return body , attachments
def message_parse ( self , cr , uid , message , save_original = False , context = None ) :
2012-08-16 15:48:23 +00:00
""" Parses a string or email.message.Message representing an
RFC - 2822 email , and returns a generic dict holding the
message details .
2011-07-22 16:34:57 +00:00
2012-08-16 15:48:23 +00:00
: param message : the message to parse
: type message : email . message . Message | string | unicode
: param bool save_original : whether the returned dict
2012-08-23 18:54:43 +00:00
should include an ` ` original ` ` attachment containing
the source of the message
2011-07-22 16:34:57 +00:00
: rtype : dict
2012-08-16 15:48:23 +00:00
: return : A dict with the following structure , where each
field may not be present if missing in original
message : :
2012-10-09 13:40:20 +00:00
{ ' message_id ' : msg_id ,
2012-08-16 15:48:23 +00:00
' subject ' : subject ,
2012-08-23 18:54:43 +00:00
' from ' : from ,
' to ' : to ,
' cc ' : cc ,
2012-08-31 08:01:03 +00:00
' body ' : unified_body ,
2012-08-16 15:48:23 +00:00
' attachments ' : [ ( ' file1 ' , ' bytes ' ) ,
2012-08-23 18:54:43 +00:00
( ' file2 ' , ' bytes ' ) }
2012-08-16 15:48:23 +00:00
}
2011-07-22 16:34:57 +00:00
"""
2012-10-25 11:30:48 +00:00
msg_dict = {
' type ' : ' email ' ,
}
2012-08-23 18:54:43 +00:00
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 ' ]
2012-08-16 15:48:23 +00:00
if not message_id :
# Very unusual situation, be we should be fault-tolerant here
2012-08-23 18:54:43 +00:00
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
2012-08-16 15:48:23 +00:00
2013-03-01 11:46:30 +00:00
if message . get ( ' Subject ' ) :
2012-08-23 18:54:43 +00:00
msg_dict [ ' subject ' ] = decode ( message . get ( ' Subject ' ) )
2012-08-16 15:48:23 +00:00
2012-10-25 13:50:20 +00:00
# Envelope fields not stored in mail.message but made available for message_new()
2012-08-23 18:54:43 +00:00
msg_dict [ ' from ' ] = decode ( message . get ( ' from ' ) )
msg_dict [ ' to ' ] = decode ( message . get ( ' to ' ) )
msg_dict [ ' cc ' ] = decode ( message . get ( ' cc ' ) )
2013-04-16 13:28:13 +00:00
msg_dict [ ' email_from ' ] = decode ( message . get ( ' from ' ) )
2013-02-13 09:59:42 +00:00
partner_ids = self . _message_find_partners ( cr , uid , message , [ ' To ' , ' Cc ' ] , context = context )
2012-11-07 10:51:48 +00:00
msg_dict [ ' partner_ids ' ] = [ ( 4 , partner_id ) for partner_id in partner_ids ]
2012-08-16 15:48:23 +00:00
2013-03-01 11:46:30 +00:00
if message . get ( ' Date ' ) :
2012-12-18 02:11:23 +00:00
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 :
2013-04-03 12:09:26 +00:00
stored_date = parsed_date . astimezone ( tz = pytz . utc )
2012-12-18 02:11:23 +00:00
except Exception :
_logger . warning ( ' 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 )
2012-08-16 15:48:23 +00:00
2013-03-01 11:46:30 +00:00
if message . get ( ' In-Reply-To ' ) :
2012-09-05 15:51:21 +00:00
parent_ids = self . pool . get ( ' mail.message ' ) . search ( cr , uid , [ ( ' message_id ' , ' = ' , decode ( message [ ' In-Reply-To ' ] ) ) ] )
2012-08-28 17:39:01 +00:00
if parent_ids :
msg_dict [ ' parent_id ' ] = parent_ids [ 0 ]
2013-03-01 11:46:30 +00:00
if message . get ( ' References ' ) and ' parent_id ' not in msg_dict :
2012-09-05 15:51:21 +00:00
parent_ids = self . pool . get ( ' mail.message ' ) . search ( cr , uid , [ ( ' message_id ' , ' in ' ,
2012-08-28 17:39:01 +00:00
[ x . strip ( ) for x in decode ( message [ ' References ' ] ) . split ( ) ] ) ] )
if parent_ids :
msg_dict [ ' parent_id ' ] = parent_ids [ 0 ]
2012-09-05 15:51:21 +00:00
2013-03-27 11:25:17 +00:00
msg_dict [ ' body ' ] , msg_dict [ ' attachments ' ] = self . _message_extract_payload ( message , save_original = save_original )
2012-08-23 18:54:43 +00:00
return msg_dict
2012-02-01 16:21:36 +00:00
#------------------------------------------------------
# Note specific
#------------------------------------------------------
2012-04-25 05:41:43 +00:00
2012-04-02 11:50:02 +00:00
def log ( self , cr , uid , id , message , secondary = False , context = None ) :
2012-09-04 13:36:48 +00:00
_logger . warning ( " log() is deprecated. As this module inherit from " \
" mail.thread, the message will be managed by this " \
" module instead of by the res.log mechanism. Please " \
" use mail_thread.message_post() instead of the " \
" now deprecated res.log. " )
2012-08-22 11:34:39 +00:00
self . message_post ( cr , uid , [ id ] , message , context = context )
2013-02-28 17:05:46 +00:00
def _message_add_suggested_recipient ( self , cr , uid , result , obj , partner = None , email = None , reason = ' ' , context = None ) :
2013-02-26 15:07:07 +00:00
""" 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 """
2013-02-28 17:05:46 +00:00
if email and not partner :
2013-03-26 12:53:11 +00:00
# get partner info from email
2013-04-16 13:28:13 +00:00
partner_info = self . message_partner_info_from_emails ( cr , uid , obj . id , [ email ] , context = context ) [ 0 ]
2013-02-28 17:05:46 +00:00
if partner_info . get ( ' partner_id ' ) :
2013-10-15 18:13:31 +00:00
partner = self . pool . get ( ' res.partner ' ) . browse ( cr , SUPERUSER_ID , [ partner_info [ ' partner_id ' ] ] , context = context ) [ 0 ]
2013-02-28 17:05:46 +00:00
if email and email in [ val [ 1 ] for val in result [ obj . id ] ] : # already existing email -> skip
return result
2013-02-26 15:15:31 +00:00
if partner and partner in obj . message_follower_ids : # recipient already in the followers -> skip
2013-02-21 18:42:43 +00:00
return result
2013-02-26 15:15:31 +00:00
if partner and partner in [ val [ 0 ] for val in result [ obj . id ] ] : # already existing partner ID -> skip
2013-02-21 18:42:43 +00:00
return result
2013-02-26 15:15:31 +00:00
if partner and partner . email : # complete profile: id, name <email>
2013-02-21 18:42:43 +00:00
result [ obj . id ] . append ( ( partner . id , ' %s < %s > ' % ( partner . name , partner . email ) , reason ) )
2013-02-26 15:15:31 +00:00
elif partner : # incomplete profile: id, name
2013-02-21 18:42:43 +00:00
result [ obj . id ] . append ( ( partner . id , ' %s ' % ( partner . name ) , reason ) )
2013-02-26 15:15:31 +00:00
else : # unknown partner, we are probably managing an email address
2013-02-21 18:42:43 +00:00
result [ obj . id ] . append ( ( False , email , reason ) )
return result
2013-02-20 12:49:23 +00:00
def message_get_suggested_recipients ( self , cr , uid , ids , context = None ) :
2013-02-26 15:07:07 +00:00
""" Returns suggested recipients for ids. Those are a list of
tuple ( partner_id , partner_name , reason ) , to be managed by Chatter . """
2013-02-21 18:42:43 +00:00
result = dict . fromkeys ( ids , list ( ) )
if self . _all_columns . get ( ' user_id ' ) :
2013-02-26 15:07:07 +00:00
for obj in self . browse ( cr , SUPERUSER_ID , ids , context = context ) : # SUPERUSER because of a read on res.users that would crash otherwise
2013-02-21 18:42:43 +00:00
if not obj . user_id or not obj . user_id . partner_id :
continue
2013-02-28 17:05:46 +00:00
self . _message_add_suggested_recipient ( cr , uid , result , obj , partner = obj . user_id . partner_id , reason = self . _all_columns [ ' user_id ' ] . column . string , context = context )
2013-02-21 18:42:43 +00:00
return result
2013-02-20 12:49:23 +00:00
2013-06-24 15:18:27 +00:00
def _find_partner_from_emails ( self , cr , uid , id , emails , model = None , context = None , check_followers = True ) :
2013-06-25 09:26:06 +00:00
""" 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
2013-05-22 14:32:06 +00:00
"""
partner_obj = self . pool [ ' res.partner ' ]
partner_ids = [ ]
obj = None
2013-06-24 15:18:27 +00:00
if id and ( model or self . _name != ' mail.thread ' ) and check_followers :
if model :
obj = self . pool [ model ] . browse ( cr , uid , id , context = context )
else :
obj = self . browse ( cr , uid , id , context = context )
2013-05-22 14:32:06 +00:00
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
if obj :
for follower in obj . message_follower_ids :
if follower . email == email_address :
partner_id = follower . id
# second try: check in partners that are also users
if not partner_id :
ids = partner_obj . search ( cr , SUPERUSER_ID , [
( ' email ' , ' ilike ' , email_address ) ,
( ' user_ids ' , ' != ' , False )
] , limit = 1 , context = context )
if ids :
partner_id = ids [ 0 ]
# third try: check in partners
if not partner_id :
ids = partner_obj . search ( cr , SUPERUSER_ID , [
( ' email ' , ' ilike ' , email_address )
] , limit = 1 , context = context )
if ids :
partner_id = ids [ 0 ]
partner_ids . append ( partner_id )
return partner_ids
2013-03-26 12:53:11 +00:00
2013-05-22 14:32:06 +00:00
def message_partner_info_from_emails ( self , cr , uid , id , emails , link_mail = False , context = None ) :
2013-01-08 16:13:32 +00:00
""" 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 .
2013-06-24 15:18:27 +00:00
: return dict : partner_ids and new_partner_ids """
2013-01-08 16:13:32 +00:00
mail_message_obj = self . pool . get ( ' mail.message ' )
2013-05-22 14:32:06 +00:00
partner_ids = self . _find_partner_from_emails ( cr , uid , id , emails , context = context )
2013-02-21 18:42:43 +00:00
result = list ( )
2013-05-22 14:32:06 +00:00
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 }
2013-02-21 18:42:43 +00:00
result . append ( partner_info )
2013-01-08 16:13:32 +00:00
# link mail with this from mail to the new partner id
2013-04-11 10:17:20 +00:00
if link_mail and partner_info [ ' partner_id ' ] :
2013-02-20 12:49:23 +00:00
message_ids = mail_message_obj . search ( cr , SUPERUSER_ID , [
' | ' ,
2013-05-22 14:32:06 +00:00
( ' email_from ' , ' = ' , email_address ) ,
( ' email_from ' , ' ilike ' , ' < %s > ' % email_address ) ,
2013-02-20 12:49:23 +00:00
( ' author_id ' , ' = ' , False )
] , context = context )
if message_ids :
2013-04-11 10:17:20 +00:00
mail_message_obj . write ( cr , SUPERUSER_ID , message_ids , { ' author_id ' : partner_info [ ' partner_id ' ] } , context = context )
2013-02-21 18:42:43 +00:00
return result
2013-01-08 16:13:32 +00:00
2013-11-26 11:09:42 +00:00
def _message_preprocess_attachments ( self , cr , uid , attachments , attachment_ids , attach_model , attach_res_id , context = None ) :
""" 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
"""
Attachment = self . pool [ ' ir.attachment ' ]
m2m_attachment_ids = [ ]
if attachment_ids :
filtered_attachment_ids = Attachment . search ( cr , SUPERUSER_ID , [
( ' res_model ' , ' = ' , ' mail.compose.message ' ) ,
( ' create_uid ' , ' = ' , uid ) ,
( ' id ' , ' in ' , attachment_ids ) ] , context = context )
if filtered_attachment_ids :
Attachment . write ( cr , SUPERUSER_ID , filtered_attachment_ids , { ' res_model ' : attach_model , ' res_id ' : attach_res_id } , context = context )
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
2012-10-01 13:05:30 +00:00
def message_post ( self , cr , uid , thread_id , body = ' ' , subject = None , type = ' notification ' ,
2013-09-13 11:54:08 +00:00
subtype = None , parent_id = False , attachments = None , context = None ,
content_subtype = ' html ' , * * kwargs ) :
2012-09-04 13:36:48 +00:00
""" Post a new message in an existing thread, returning the new
2013-02-21 15:08:49 +00:00
mail . message ID .
2013-02-21 13:26:47 +00:00
2012-11-07 15:39:10 +00:00
: 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
2012-08-31 08:01:03 +00:00
: param str body : body of the message , usually raw HTML that will
be sanitized
2013-02-21 15:08:49 +00:00
: 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
2012-10-08 14:26:54 +00:00
: param tuple ( str , str ) attachments or list id : list of attachment tuples in the form
2012-08-31 08:01:03 +00:00
` ` ( name , content ) ` ` , where content is NOT base64 encoded
2013-02-21 15:08:49 +00:00
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
2012-08-22 11:34:39 +00:00
"""
2012-11-08 15:25:02 +00:00
if context is None :
context = { }
if attachments is None :
attachments = { }
2012-10-15 13:23:13 +00:00
mail_message = self . pool . get ( ' mail.message ' )
2013-02-21 10:15:15 +00:00
ir_attachment = self . pool . get ( ' ir.attachment ' )
2012-11-08 15:25:02 +00:00
2013-02-21 15:08:49 +00:00
assert ( not thread_id ) or \
isinstance ( thread_id , ( int , long ) ) or \
( isinstance ( thread_id , ( list , tuple ) ) and len ( thread_id ) == 1 ) , \
" Invalid thread_id; should be 0, False, an ID or a list with one ID "
2012-08-22 11:34:39 +00:00
if isinstance ( thread_id , ( list , tuple ) ) :
2013-02-21 15:08:49 +00:00
thread_id = thread_id [ 0 ]
2013-02-14 12:02:57 +00:00
# if we're processing a message directly coming from the gateway, the destination model was
2013-02-21 09:59:06 +00:00
# set in the context.
2013-02-14 12:02:57 +00:00
model = False
if thread_id :
model = context . get ( ' thread_model ' , self . _name ) if self . _name == ' mail.thread ' else self . _name
2013-06-06 12:32:30 +00:00
if model != self . _name and hasattr ( self . pool [ model ] , ' message_post ' ) :
2013-03-13 08:38:08 +00:00
del context [ ' thread_model ' ]
2013-03-29 14:37:20 +00:00
return self . pool [ model ] . message_post ( cr , uid , thread_id , body = body , subject = subject , type = type , subtype = subtype , parent_id = parent_id , attachments = attachments , context = context , content_subtype = content_subtype , * * kwargs )
2012-08-17 10:03:02 +00:00
2013-04-16 13:28:13 +00:00
#0: Find the message's author, because we need it for private discussion
2013-04-03 12:13:07 +00:00
author_id = kwargs . get ( ' author_id ' )
if author_id is None : # keep False values
author_id = self . pool . get ( ' mail.message ' ) . _get_default_author ( cr , uid , context = context )
2012-08-17 10:03:02 +00:00
2013-02-21 13:26:47 +00:00
# 1: Handle content subtype: if plaintext, converto into HTML
if content_subtype == ' plaintext ' :
body = tools . plaintext2html ( body )
2013-04-03 12:13:07 +00:00
# 2: Private message: add recipients (recipients and author of parent message) - current author
2013-03-06 16:28:11 +00:00
# + legacy-code management (! we manage only 4 and 6 commands)
partner_ids = set ( )
2013-03-07 09:31:15 +00:00
kwargs_partner_ids = kwargs . pop ( ' partner_ids ' , [ ] )
2013-03-06 16:28:11 +00:00
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 :
2013-03-07 09:34:14 +00:00
partner_ids | = set ( partner_id [ 2 ] )
2013-03-06 16:28:11 +00:00
elif isinstance ( partner_id , ( int , long ) ) :
partner_ids . add ( partner_id )
else :
pass # we do not manage anything else
2013-04-03 08:51:16 +00:00
if parent_id and not model :
2013-02-21 13:26:47 +00:00
parent_message = mail_message . browse ( cr , uid , parent_id , context = context )
2013-04-03 12:13:07 +00:00
private_followers = set ( [ partner . id for partner in parent_message . partner_ids ] )
2013-02-21 13:26:47 +00:00
if parent_message . author_id :
2013-04-03 12:13:07 +00:00
private_followers . add ( parent_message . author_id . id )
private_followers - = set ( [ author_id ] )
partner_ids | = private_followers
2013-02-21 13:26:47 +00:00
# 3. Attachments
# - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
2013-11-26 11:09:42 +00:00
attachment_ids = self . _message_preprocess_attachments ( cr , uid , attachments , kwargs . pop ( ' attachment_ids ' , [ ] ) , model , thread_id , context )
2012-08-17 10:03:02 +00:00
2013-02-21 13:26:47 +00:00
# 4: mail.message.subtype
2013-02-21 15:08:49 +00:00
subtype_id = False
2012-10-15 13:23:13 +00:00
if subtype :
2013-02-21 15:08:49 +00:00
if ' . ' not in subtype :
subtype = ' mail. %s ' % subtype
ref = self . pool . get ( ' ir.model.data ' ) . get_object_reference ( cr , uid , * subtype . split ( ' . ' ) )
2012-10-15 13:23:13 +00:00
subtype_id = ref and ref [ 1 ] or False
2013-02-21 13:09:00 +00:00
# automatically subscribe recipients if asked to
2013-02-21 15:08:49 +00:00
if context . get ( ' mail_post_autofollow ' ) and thread_id and partner_ids :
2013-02-28 16:40:54 +00:00
partner_to_subscribe = partner_ids
if context . get ( ' mail_post_autofollow_partner_ids ' ) :
partner_to_subscribe = filter ( lambda item : item in context . get ( ' mail_post_autofollow_partner_ids ' ) , partner_ids )
self . message_subscribe ( cr , uid , [ thread_id ] , list ( partner_to_subscribe ) , context = context )
2012-09-25 08:58:09 +00:00
2012-10-15 13:23:13 +00:00
# _mail_flat_thread: automatically set free messages to the first posted message
if self . _mail_flat_thread and not parent_id and thread_id :
message_ids = mail_message . search ( cr , uid , [ ' & ' , ( ' res_id ' , ' = ' , thread_id ) , ( ' model ' , ' = ' , model ) ] , context = context , order = " id ASC " , limit = 1 )
parent_id = message_ids and message_ids [ 0 ] or False
2012-10-25 15:11:23 +00:00
# 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 :
message_ids = mail_message . search ( cr , SUPERUSER_ID , [ ( ' id ' , ' = ' , parent_id ) , ( ' parent_id ' , ' != ' , False ) ] , context = context )
2012-10-26 12:36:04 +00:00
# avoid loops when finding ancestors
processed_list = [ ]
2012-10-25 15:42:15 +00:00
if message_ids :
message = mail_message . browse ( cr , SUPERUSER_ID , message_ids [ 0 ] , context = context )
2012-10-26 12:36:04 +00:00
while ( message . parent_id and message . parent_id . id not in processed_list ) :
processed_list . append ( message . parent_id . id )
2012-10-25 15:42:15 +00:00
message = message . parent_id
parent_id = message . id
2012-09-25 08:58:09 +00:00
2012-08-22 11:34:39 +00:00
values = kwargs
2012-09-05 15:51:21 +00:00
values . update ( {
2013-04-03 12:13:07 +00:00
' author_id ' : author_id ,
2012-09-25 08:58:09 +00:00
' model ' : model ,
2012-08-22 12:49:43 +00:00
' res_id ' : thread_id or False ,
2012-08-17 10:03:02 +00:00
' body ' : body ,
2012-10-01 13:05:30 +00:00
' subject ' : subject or False ,
2012-09-04 13:36:48 +00:00
' type ' : type ,
2012-08-21 10:43:45 +00:00
' parent_id ' : parent_id ,
2012-08-31 08:01:03 +00:00
' attachment_ids ' : attachment_ids ,
2012-09-20 10:17:04 +00:00
' subtype_id ' : subtype_id ,
2013-02-21 14:24:17 +00:00
' partner_ids ' : [ ( 4 , pid ) for pid in partner_ids ] ,
2012-08-17 10:03:02 +00:00
} )
2012-10-03 14:27:12 +00:00
2012-09-20 10:17:04 +00:00
# Avoid warnings about non-existing fields
for x in ( ' from ' , ' to ' , ' cc ' ) :
values . pop ( x , None )
2012-09-25 08:58:09 +00:00
2013-02-21 15:08:49 +00:00
# Create and auto subscribe the author
msg_id = mail_message . create ( cr , uid , values , context = context )
message = mail_message . browse ( cr , uid , msg_id , context = context )
2013-03-15 12:40:09 +00:00
if message . author_id and thread_id and type != ' notification ' and not context . get ( ' mail_create_nosubscribe ' ) :
2013-02-21 15:08:49 +00:00
self . message_subscribe ( cr , uid , [ thread_id ] , [ message . author_id . id ] , context = context )
return msg_id
2012-04-25 05:41:43 +00:00
2012-10-15 13:23:13 +00:00
#------------------------------------------------------
# Followers API
#------------------------------------------------------
2012-09-27 13:48:23 +00:00
2013-06-05 13:10:31 +00:00
def message_get_subscription_data ( self , cr , uid , ids , user_pid = None , context = None ) :
2012-10-18 13:06:01 +00:00
""" Wrapper to get subtypes data. """
2013-06-05 13:10:31 +00:00
return self . _get_subscription_data ( cr , uid , ids , None , None , user_pid = user_pid , context = context )
2012-09-25 08:58:09 +00:00
2012-09-20 10:17:04 +00:00
def message_subscribe_users ( self , cr , uid , ids , user_ids = None , subtype_ids = None , context = None ) :
2012-08-22 11:03:13 +00:00
""" Wrapper on message_subscribe, using users. If user_ids is not
provided , subscribe uid instead . """
2012-10-15 13:23:13 +00:00
if user_ids is None :
user_ids = [ uid ]
2012-08-22 11:03:13 +00:00
partner_ids = [ user . partner_id . id for user in self . pool . get ( ' res.users ' ) . browse ( cr , uid , user_ids , context = context ) ]
2012-09-20 10:17:04 +00:00
return self . message_subscribe ( cr , uid , ids , partner_ids , subtype_ids = subtype_ids , context = context )
2012-08-16 10:18:48 +00:00
2012-09-20 10:17:04 +00:00
def message_subscribe ( self , cr , uid , ids , partner_ids , subtype_ids = None , context = None ) :
2012-09-12 13:37:11 +00:00
""" Add partners to the records followers. """
2014-01-10 10:01:33 +00:00
if context is None :
context = { }
2013-06-13 08:20:29 +00:00
mail_followers_obj = self . pool . get ( ' mail.followers ' )
subtype_obj = self . pool . get ( ' mail.message.subtype ' )
user_pid = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . partner_id . id
2012-12-21 11:00:03 +00:00
if set ( partner_ids ) == set ( [ user_pid ] ) :
2014-01-10 10:01:33 +00:00
if context . get ( ' operation ' , ' ' ) != ' create ' :
try :
self . check_access_rights ( cr , uid , ' read ' )
self . check_access_rule ( cr , uid , ids , ' read ' )
except ( osv . except_osv , orm . except_orm ) :
return False
2012-12-21 11:00:03 +00:00
else :
self . check_access_rights ( cr , uid , ' write ' )
2014-01-06 14:11:57 +00:00
self . check_access_rule ( cr , uid , ids , ' write ' )
2012-12-21 11:14:00 +00:00
2013-11-26 17:17:52 +00:00
existing_pids_dict = { }
2014-01-09 13:17:48 +00:00
fol_ids = mail_followers_obj . search ( cr , SUPERUSER_ID , [ ' & ' , ' & ' , ( ' res_model ' , ' = ' , self . _name ) , ( ' res_id ' , ' in ' , ids ) , ( ' partner_id ' , ' in ' , partner_ids ) ] )
2013-11-26 17:17:52 +00:00
for fol in mail_followers_obj . browse ( cr , SUPERUSER_ID , fol_ids , context = context ) :
existing_pids_dict . setdefault ( fol . res_id , set ( ) ) . add ( fol . partner_id . id )
# subtype_ids specified: update already subscribed partners
if subtype_ids and fol_ids :
mail_followers_obj . write ( cr , SUPERUSER_ID , fol_ids , { ' subtype_ids ' : [ ( 6 , 0 , subtype_ids ) ] } , context = context )
# subtype_ids not specified: do not update already subscribed partner, fetch default subtypes for new partners
if subtype_ids is None :
subtype_ids = subtype_obj . search (
cr , uid , [
( ' default ' , ' = ' , True ) , ' | ' , ( ' res_model ' , ' = ' , self . _name ) , ( ' res_model ' , ' = ' , False ) ] , context = context )
for id in ids :
existing_pids = existing_pids_dict . get ( id , set ( ) )
2013-06-13 08:20:29 +00:00
new_pids = set ( partner_ids ) - existing_pids
# subscribe new followers
for new_pid in new_pids :
2013-11-26 17:17:52 +00:00
mail_followers_obj . create (
cr , SUPERUSER_ID , {
' res_model ' : self . _name ,
' res_id ' : id ,
' partner_id ' : new_pid ,
' subtype_ids ' : [ ( 6 , 0 , subtype_ids ) ] ,
} , context = context )
2013-05-08 10:23:04 +00:00
2012-09-20 10:17:04 +00:00
return True
2012-02-01 16:21:36 +00:00
2012-08-22 11:03:13 +00:00
def message_unsubscribe_users ( self , cr , uid , ids , user_ids = None , context = None ) :
""" Wrapper on message_subscribe, using users. If user_ids is not
provided , unsubscribe uid instead . """
2012-10-15 13:23:13 +00:00
if user_ids is None :
2012-09-20 10:17:04 +00:00
user_ids = [ uid ]
2012-08-22 11:03:13 +00:00
partner_ids = [ user . partner_id . id for user in self . pool . get ( ' res.users ' ) . browse ( cr , uid , user_ids , context = context ) ]
return self . message_unsubscribe ( cr , uid , ids , partner_ids , context = context )
2012-08-15 13:36:43 +00:00
2012-08-22 11:03:13 +00:00
def message_unsubscribe ( self , cr , uid , ids , partner_ids , context = None ) :
2012-09-12 13:37:11 +00:00
""" Remove partners from the records followers. """
2012-12-21 11:00:03 +00:00
user_pid = self . pool . get ( ' res.users ' ) . read ( cr , uid , uid , [ ' partner_id ' ] , context = context ) [ ' partner_id ' ] [ 0 ]
if set ( partner_ids ) == set ( [ user_pid ] ) :
2012-12-20 14:09:46 +00:00
self . check_access_rights ( cr , uid , ' read ' )
2014-01-06 14:11:57 +00:00
self . check_access_rule ( cr , uid , ids , ' read ' )
2012-12-20 14:09:46 +00:00
else :
self . check_access_rights ( cr , uid , ' write ' )
2014-01-06 14:11:57 +00:00
self . check_access_rule ( cr , uid , ids , ' write ' )
2013-11-26 17:17:52 +00:00
fol_obj = self . pool [ ' mail.followers ' ]
fol_ids = fol_obj . search (
cr , SUPERUSER_ID , [
( ' res_model ' , ' = ' , self . _name ) ,
( ' res_id ' , ' in ' , ids ) ,
( ' partner_id ' , ' in ' , partner_ids )
] , context = context )
return fol_obj . unlink ( cr , SUPERUSER_ID , fol_ids , context = context )
2012-02-01 16:21:36 +00:00
2013-01-30 13:27:23 +00:00
def _message_get_auto_subscribe_fields ( self , cr , uid , updated_fields , auto_follow_fields = [ ' user_id ' ] , context = 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 .
"""
user_field_lst = [ ]
for name , column_info in self . _all_columns . items ( ) :
if name in auto_follow_fields and name in updated_fields and getattr ( column_info . column , ' track_visibility ' , False ) and column_info . column . _obj == ' res.users ' :
user_field_lst . append ( name )
return user_field_lst
2013-11-14 11:32:31 +00:00
def message_auto_subscribe ( self , cr , uid , ids , updated_fields , context = None , 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 .
2012-12-19 16:42:39 +00:00
"""
2012-12-18 15:34:57 +00:00
subtype_obj = self . pool . get ( ' mail.message.subtype ' )
follower_obj = self . pool . get ( ' mail.followers ' )
2013-11-14 11:32:31 +00:00
new_followers = dict ( )
2012-12-18 15:34:57 +00:00
2013-11-14 11:32:31 +00:00
# fetch auto_follow_fields: res.users relation fields whose changes are tracked for subscription
2013-01-30 13:27:23 +00:00
user_field_lst = self . _message_get_auto_subscribe_fields ( cr , uid , updated_fields , context = context )
2013-01-30 09:09:36 +00:00
2013-11-14 11:32:31 +00:00
# fetch header subtypes
header_subtype_ids = subtype_obj . search ( cr , uid , [ ' | ' , ( ' res_model ' , ' = ' , False ) , ( ' parent_id.res_model ' , ' = ' , self . _name ) ] , context = context )
subtypes = subtype_obj . browse ( cr , uid , header_subtype_ids , context = context )
# 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 :
2012-12-19 16:42:39 +00:00
return True
2012-12-18 15:34:57 +00:00
2013-11-14 11:32:31 +00:00
# legacy behavior: if values is not given, compute the values by browsing
# @TDENOTE: remove me in 8.0
if values is None :
record = self . browse ( cr , uid , ids [ 0 ] , context = context )
for updated_field in updated_fields :
field_value = getattr ( record , updated_field )
if isinstance ( field_value , browse_record ) :
field_value = field_value . id
elif isinstance ( field_value , browse_null ) :
field_value = False
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 :
2013-11-14 12:17:46 +00:00
header_domain + = [ ' & ' , ( ' res_model ' , ' = ' , header [ 0 ] ) , ( ' res_id ' , ' = ' , header [ 1 ] ) ]
2013-11-14 11:32:31 +00:00
header_follower_ids = follower_obj . search (
cr , SUPERUSER_ID ,
header_domain ,
context = context
)
for header_follower in follower_obj . browse ( cr , SUPERUSER_ID , header_follower_ids , context = context ) :
for subtype in header_follower . subtype_ids :
2013-11-20 15:43:20 +00:00
if subtype . parent_id and subtype . parent_id . res_model == self . _name :
2013-11-14 11:32:31 +00:00
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 . pool . get ( ' res.users ' ) . browse ( cr , SUPERUSER_ID , user_ids , context = context ) ]
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 ( cr , uid , ids , [ pid ] , subtypes , context = context )
# find first email message, set it as unread for auto_subscribe fields for them to have a notification
if user_pids :
for record_id in ids :
message_obj = self . pool . get ( ' mail.message ' )
msg_ids = message_obj . search ( cr , SUPERUSER_ID , [
( ' model ' , ' = ' , self . _name ) ,
( ' res_id ' , ' = ' , record_id ) ,
( ' type ' , ' = ' , ' email ' ) ] , limit = 1 , context = context )
if not msg_ids :
msg_ids = message_obj . search ( cr , SUPERUSER_ID , [
( ' model ' , ' = ' , self . _name ) ,
( ' res_id ' , ' = ' , record_id ) ] , limit = 1 , context = context )
2013-02-22 12:56:10 +00:00
if msg_ids :
2013-11-14 11:32:31 +00:00
self . pool . get ( ' mail.notification ' ) . _notify ( cr , uid , msg_ids [ 0 ] , partners_to_notify = user_pids , context = context )
2013-02-22 12:56:10 +00:00
2012-12-19 16:42:39 +00:00
return True
2012-12-18 13:11:42 +00:00
2012-03-21 17:20:18 +00:00
#------------------------------------------------------
2012-08-28 12:55:22 +00:00
# Thread state
2012-06-04 09:33:24 +00:00
#------------------------------------------------------
2012-06-07 15:17:53 +00:00
2012-08-17 13:34:49 +00:00
def message_mark_as_unread ( self , cr , uid , ids , context = None ) :
2012-08-28 12:55:22 +00:00
""" Set as unread. """
2012-08-17 13:34:49 +00:00
partner_id = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . partner_id . id
cr . execute ( '''
2012-09-05 15:51:21 +00:00
UPDATE mail_notification SET
2012-08-17 13:34:49 +00:00
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
''' , (ids, self._name, partner_id))
return True
2011-07-22 16:34:57 +00:00
2012-06-25 16:13:12 +00:00
def message_mark_as_read ( self , cr , uid , ids , context = None ) :
2012-07-02 15:46:30 +00:00
""" Set as read. """
2012-08-17 13:34:49 +00:00
partner_id = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . partner_id . id
2012-08-15 13:36:43 +00:00
cr . execute ( '''
2012-09-05 15:51:21 +00:00
UPDATE mail_notification SET
2012-08-15 13:36:43 +00:00
read = true
2012-08-17 13:34:49 +00:00
WHERE
message_id IN ( SELECT id FROM mail_message WHERE res_id = ANY ( % s ) AND model = % s ) AND
partner_id = % s
''' , (ids, self._name, partner_id))
2012-08-15 13:36:43 +00:00
return True
2012-06-25 16:13:12 +00:00
2013-04-03 12:24:08 +00:00
#------------------------------------------------------
# Thread suggestion
#------------------------------------------------------
def get_suggested_thread ( self , cr , uid , removed_suggested_threads = None , context = None ) :
""" Return a list of suggested threads, sorted by the numbers of followers """
if context is None :
context = { }
2013-04-05 14:16:18 +00:00
2013-06-12 11:36:30 +00:00
# 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 self . pool [ ' res.groups ' ] . _all_columns . get ( ' is_portal ' ) :
user = self . pool . get ( ' res.users ' ) . browse ( cr , SUPERUSER_ID , uid , context = context )
if any ( group . is_portal for group in user . groups_id ) :
return [ ]
2013-04-03 12:24:08 +00:00
threads = [ ]
if removed_suggested_threads is None :
removed_suggested_threads = [ ]
2013-05-02 12:39:45 +00:00
thread_ids = self . search ( cr , uid , [ ( ' id ' , ' not in ' , removed_suggested_threads ) , ( ' message_is_follower ' , ' = ' , False ) ] , context = context )
for thread in self . browse ( cr , uid , thread_ids , context = context ) :
data = {
' id ' : thread . id ,
' popularity ' : len ( thread . message_follower_ids ) ,
' name ' : thread . name ,
' image_small ' : thread . image_small
}
threads . append ( data )
2013-06-12 11:36:30 +00:00
return sorted ( threads , key = lambda x : ( x [ ' popularity ' ] , x [ ' id ' ] ) , reverse = True ) [ : 3 ]