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-08-22 08:38:13 +00:00
import dateutil
2011-07-22 16:34:57 +00:00
import email
import logging
2012-08-23 18:54:43 +00:00
import pytz
2012-04-05 21:04:43 +00:00
import time
2012-08-28 12:55:22 +00:00
import tools
2012-08-09 17:16:55 +00:00
import xmlrpclib
2012-08-28 12:55:22 +00:00
2012-08-09 17:16:55 +00:00
from email . message import Message
2012-08-23 18:54:43 +00:00
from mail_message import decode
2012-08-09 17:16:55 +00:00
from osv import osv , fields
from tools . safe_eval import safe_eval as eval
2011-07-22 16:34:57 +00:00
2012-02-01 16:21:36 +00:00
_logger = logging . getLogger ( __name__ )
2011-07-22 16:34:57 +00:00
2012-08-10 13:19:19 +00:00
def decode_header ( message , header , separator = ' ' ) :
2012-09-05 15:51:21 +00:00
return separator . join ( map ( decode , message . get_all ( header , [ ] ) ) )
2012-08-10 13:19:19 +00:00
2012-08-10 13:03:51 +00:00
class many2many_reference ( fields . many2many ) :
2012-09-04 12:18:38 +00:00
""" many2many_reference manages many2many fields where one id is found
by a reference - like key ( a char column in addition to the foreign id ) .
The reference_column attribute on the many2many fields is used ;
if not defined , ` ` res_model ` ` is used . """
2012-09-05 15:51:21 +00:00
2012-08-09 14:43:45 +00:00
def _get_query_and_where_params ( self , cr , model , ids , values , where_params ) :
2012-09-04 12:18:38 +00:00
""" Add in where condition like mail_followers.res_model = ' crm.lead ' """
reference_column = self . reference_column if self . reference_column else ' res_model '
values . update ( reference_column = reference_column , reference_value = model . _name )
2012-08-09 14:43:45 +00:00
query = ' SELECT %(rel)s . %(id2)s , %(rel)s . %(id1)s \
2012-08-10 13:03:51 +00:00
FROM % ( rel ) s , % ( from_c ) s \
WHERE % ( rel ) s . % ( id1 ) s IN % % s \
2012-08-09 14:43:45 +00:00
AND % ( rel ) s . % ( id2 ) s = % ( tbl ) s . id \
2012-09-04 12:18:38 +00:00
AND % ( rel ) s . % ( reference_column ) s = \' %(reference_value)s \' \
2012-08-10 13:03:51 +00:00
% ( where_c ) s \
% ( order_by ) s \
% ( limit ) s \
OFFSET % ( offset ) d ' \
% values
2012-08-09 14:43:45 +00:00
return query , where_params
2012-08-10 13:03:51 +00:00
def set ( self , cr , model , id , name , values , user = None , context = None ) :
2012-09-04 12:18:38 +00:00
""" Override to add the reference field in queries. """
2012-08-10 13:03:51 +00:00
if not values : return
rel , id1 , id2 = self . _sql_names ( model )
obj = model . pool . get ( self . _obj )
2012-08-23 09:59:20 +00:00
# reference column name: given by attribute or res_model
2012-09-04 12:18:38 +00:00
reference_column = self . reference_column if self . reference_column else ' res_model '
2012-08-10 13:03:51 +00:00
for act in values :
if not ( isinstance ( act , list ) or isinstance ( act , tuple ) ) or not act :
continue
2012-08-13 14:55:26 +00:00
if act [ 0 ] == 0 :
idnew = obj . create ( cr , user , act [ 2 ] , context = context )
2012-09-04 12:18:38 +00:00
cr . execute ( ' INSERT INTO ' + rel + ' ( ' + id1 + ' , ' + id2 + ' , ' + reference_column + ' ) VALUES ( %s , %s , %s ) ' , ( id , idnew , model . _name ) )
2012-08-13 14:55:26 +00:00
elif act [ 0 ] == 3 :
2012-09-04 12:18:38 +00:00
cr . execute ( ' DELETE FROM ' + rel + ' WHERE ' + id1 + ' = %s AND ' + id2 + ' = %s AND ' + reference_column + ' = %s ' , ( id , act [ 1 ] , model . _name ) )
2012-08-13 14:55:26 +00:00
elif act [ 0 ] == 4 :
# following queries are in the same transaction - so should be relatively safe
2012-09-04 12:18:38 +00:00
cr . execute ( ' SELECT 1 FROM ' + rel + ' WHERE ' + id1 + ' = %s AND ' + id2 + ' = %s AND ' + reference_column + ' = %s ' , ( id , act [ 1 ] , model . _name ) )
2012-08-13 14:55:26 +00:00
if not cr . fetchone ( ) :
2012-09-04 12:18:38 +00:00
cr . execute ( ' INSERT INTO ' + rel + ' ( ' + id1 + ' , ' + id2 + ' , ' + reference_column + ' ) VALUES ( %s , %s , %s ) ' , ( id , act [ 1 ] , model . _name ) )
2012-08-23 09:59:20 +00:00
elif act [ 0 ] == 5 :
2012-09-04 12:18:38 +00:00
cr . execute ( ' delete from ' + rel + ' where ' + id1 + ' = %s AND ' + reference_column + ' = %s ' , ( id , model . _name ) )
2012-08-13 18:09:41 +00:00
elif act [ 0 ] == 6 :
d1 , d2 , tables = obj . pool . get ( ' ir.rule ' ) . domain_get ( cr , user , obj . _name , context = context )
if d1 :
d1 = ' and ' + ' and ' . join ( d1 )
else :
d1 = ' '
2012-09-04 12:18:38 +00:00
cr . execute ( ' DELETE FROM ' + rel + ' WHERE ' + id1 + ' = %s AND ' + reference_column + ' = %s AND ' + id2 + ' IN (SELECT ' + rel + ' . ' + id2 + ' FROM ' + rel + ' , ' + ' , ' . join ( tables ) + ' WHERE ' + rel + ' . ' + id1 + ' = %s AND ' + rel + ' . ' + id2 + ' = ' + obj . _table + ' .id ' + d1 + ' ) ' , [ id , model . _name , id ] + d2 )
2012-08-13 18:09:41 +00:00
for act_nbr in act [ 2 ] :
2012-09-04 12:18:38 +00:00
cr . execute ( ' INSERT INTO ' + rel + ' ( ' + id1 + ' , ' + id2 + ' , ' + reference_column + ' ) VALUES ( %s , %s , %s ) ' , ( id , act_nbr , model . _name ) )
2012-08-23 09:59:20 +00:00
# cases 1, 2: performs write and unlink -> default implementation is ok
2012-08-10 13:03:51 +00:00
else :
return super ( many2many_reference , self ) . set ( cr , model , id , name , values , user , context )
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 .
2011-07-22 16:34:57 +00:00
'''
_name = ' mail.thread '
_description = ' Email Thread '
2012-04-25 05:41:43 +00:00
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 """
2012-09-05 15:51:21 +00:00
res = dict ( ( id , dict ( message_unread = False , message_summary = ' ' ) ) for id in ids )
2012-08-22 11:03:13 +00:00
notif_obj = self . pool . get ( ' mail.notification ' )
notif_ids = notif_obj . search ( cr , uid , [
( ' partner_id.user_ids ' , ' in ' , [ uid ] ) ,
( ' message_id.res_id ' , ' in ' , ids ) ,
( ' message_id.model ' , ' = ' , self . _name ) ,
( ' read ' , ' = ' , False )
2012-08-15 13:36:43 +00:00
] , context = context )
2012-08-22 11:03:13 +00:00
for notif in notif_obj . browse ( cr , uid , notif_ids , context = context ) :
2012-08-20 12:55:25 +00:00
res [ notif . message_id . res_id ] [ ' message_unread ' ] = True
2012-04-25 05:41:43 +00:00
2012-08-13 19:13:46 +00:00
for thread in self . browse ( cr , uid , ids , context = context ) :
2012-08-23 16:04:16 +00:00
cls = res [ thread . id ] [ ' message_unread ' ] and ' class= " oe_kanban_mail_new " ' or ' '
res [ thread . id ] [ ' message_summary ' ] = " <span %s ><span class= ' oe_e ' >9</span> %d </span> <span><span class= ' oe_e ' >+</span> %d </span> " % ( cls , len ( thread . message_comment_ids ) , len ( thread . message_follower_ids ) )
2012-09-20 10:17:04 +00:00
return res
def _get_subscription_data ( self , cr , uid , ids , name , args , context = None ) :
""" Computes:
- message_is_follower : is uid in the document followers
- message_subtype_data : data about document subtypes : which are
available , which are followed if any """
res = dict ( ( id , dict ( message_subtype_data = ' ' ) ) for id in ids )
user_pid = self . pool . get ( ' res.users ' ) . read ( cr , uid , uid , [ ' partner_id ' ] , context = context ) [ ' partner_id ' ] [ 0 ]
# 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 )
subtype_dict = dict ( ( subtype . name , dict ( default = subtype . default , followed = False ) ) for subtype in subtype_obj . browse ( cr , uid , subtype_ids , context = context ) )
# find the document followers, update the data
fol_obj = self . pool . get ( ' mail.followers ' )
fol_ids = fol_obj . search ( cr , uid , [
( ' partner_id ' , ' = ' , user_pid ) ,
( ' res_id ' , ' in ' , ids ) ,
( ' res_model ' , ' = ' , self . _name ) ,
] , context = context )
for fol in fol_obj . browse ( cr , uid , fol_ids , context = context ) :
thread_subtype_dict = subtype_dict . copy ( )
res [ fol . res_id ] [ ' message_is_follower ' ] = True
for subtype in fol . subtype_ids :
thread_subtype_dict [ subtype . name ] [ ' followed ' ] = True
res [ fol . res_id ] [ ' message_subtype_data ' ] = ' %s ' % thread_subtype_dict
2012-02-03 11:21:16 +00:00
return res
2012-04-25 05:41:43 +00:00
2012-08-22 15:59:54 +00:00
def _search_unread ( self , cr , uid , obj = None , name = None , domain = None , context = None ) :
partner_id = self . pool . get ( ' res.users ' ) . browse ( cr , uid , uid , context = context ) . partner_id . id
res = { }
notif_obj = self . pool . get ( ' mail.notification ' )
notif_ids = notif_obj . search ( cr , uid , [
( ' partner_id ' , ' = ' , partner_id ) ,
( ' message_id.model ' , ' = ' , self . _name ) ,
( ' read ' , ' = ' , False )
] , context = context )
for notif in notif_obj . browse ( cr , uid , notif_ids , context = context ) :
res [ notif . message_id . res_id ] = True
2012-09-05 15:51:21 +00:00
return [ ( ' id ' , ' in ' , res . keys ( ) ) ]
2012-06-14 10:09:22 +00:00
2011-07-22 16:34:57 +00:00
_columns = {
2012-09-20 10:17:04 +00:00
' message_is_follower ' : fields . function ( _get_subscription_data ,
type = ' boolean ' , string = ' Is a Follower ' , multi = ' _get_subscription_data, ' ) ,
' message_subtype_data ' : fields . function ( _get_subscription_data ,
type = ' text ' , string = ' Subscription data ' , multi = " _get_subscription_data " ,
help = " Holds data about the subtypes. The content of this field " \
" is a structure holding the current model subtypes, and the " \
" current document followed subtypes. " ) ,
2012-08-23 09:59:20 +00:00
' message_follower_ids ' : many2many_reference ( ' res.partner ' ,
' mail_followers ' , ' res_id ' , ' partner_id ' ,
2012-09-04 12:18:38 +00:00
reference_column = ' res_model ' , string = ' Followers ' ) ,
2012-08-23 15:57:47 +00:00
' message_comment_ids ' : fields . one2many ( ' mail.message ' , ' res_id ' ,
2012-09-04 13:36:48 +00:00
domain = lambda self : [ ( ' model ' , ' = ' , self . _name ) , ( ' type ' , ' in ' , ( ' comment ' , ' email ' ) ) ] ,
2012-09-05 15:51:21 +00:00
string = ' Comments and emails ' ,
2012-09-04 13:36:48 +00:00
help = " Comments and emails " ) ,
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 ) ] ,
string = ' Messages ' ,
2012-09-04 13:36:48 +00:00
help = " Messages and communication history " ) ,
2012-09-05 15:51:21 +00:00
' message_unread ' : fields . function ( _get_message_data , fnct_search = _search_unread ,
2012-09-04 13:36:48 +00:00
type = ' boolean ' , string = ' Unread Messages ' , multi = " _get_message_data " ,
help = " If checked new messages require your attention. " ) ,
2012-08-14 11:39:37 +00:00
' message_summary ' : fields . function ( _get_message_data , method = True ,
2012-08-15 13:36:43 +00:00
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
}
2012-08-15 23:38:31 +00:00
2012-02-28 14:06:32 +00:00
#------------------------------------------------------
2012-08-22 11:03:13 +00:00
# Automatic subscription when creating
2012-02-28 14:06:32 +00:00
#------------------------------------------------------
2012-04-25 05:41:43 +00:00
2012-02-28 14:06:32 +00:00
def create ( self , cr , uid , vals , context = None ) :
2012-09-04 13:36:48 +00:00
""" Override to subscribe the current user. """
2012-06-04 09:33:24 +00:00
thread_id = super ( mail_thread , self ) . create ( cr , uid , vals , context = context )
2012-08-16 10:18:48 +00:00
self . message_subscribe_users ( cr , uid , [ thread_id ] , [ uid ] , context = context )
2012-06-04 09:33:24 +00:00
return thread_id
2012-04-25 05:41:43 +00:00
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 )
# delete followers
fol_ids = fol_obj . search ( cr , uid , [ ( ' res_model ' , ' = ' , self . _name ) , ( ' res_id ' , ' in ' , ids ) ] , context = context )
fol_obj . unlink ( cr , uid , fol_ids , context = context )
2012-03-22 12:08:36 +00:00
return super ( mail_thread , self ) . unlink ( cr , uid , ids , context = context )
2012-04-25 05:41:43 +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
2012-07-03 12:20:20 +00:00
#------------------------------------------------------
2012-08-20 07:42:42 +00:00
# Mail gateway
2012-07-03 12:20:20 +00:00
#------------------------------------------------------
2012-09-04 14:50:11 +00:00
def message_capable_models ( self , cr , uid , context = None ) :
""" Used by the plugin addon, based for plugin_outlook and others. """
ret_dict = { }
for model_name in self . pool . obj_list ( ) :
model = self . pool . get ( model_name )
if ' mail.thread ' in getattr ( model , ' _inherit ' , [ ] ) :
ret_dict [ model_name ] = model . _description
return ret_dict
2012-08-22 08:38:13 +00:00
def _message_find_partners ( self , cr , uid , message , header_fields = [ ' From ' ] , context = None ) :
""" Find partners related to some header fields of the message. """
s = ' , ' . join ( [ decode ( message . get ( h ) ) for h in header_fields if message . get ( h ) ] )
2012-09-04 13:36:48 +00:00
return [ partner_id for email in tools . email_split ( s )
for partner_id in self . pool . get ( ' res.partner ' ) . search ( cr , uid , [ ( ' email ' , ' ilike ' , email ) ] , context = context ) ]
2012-04-25 05:41:43 +00:00
2012-08-07 18:04:12 +00:00
def _message_find_user_id ( self , cr , uid , message , context = None ) :
2012-08-16 16:43:11 +00:00
from_local_part = tools . email_split ( decode ( message . get ( ' From ' ) ) ) [ 0 ]
2012-08-20 07:42:42 +00:00
# FP Note: canonification required, the minimu: .lower()
2012-09-05 15:51:21 +00:00
user_ids = self . pool . get ( ' res.users ' ) . search ( cr , uid , [ ' | ' ,
2012-08-20 07:42:42 +00:00
( ' login ' , ' = ' , from_local_part ) ,
( ' email ' , ' = ' , from_local_part ) ] , context = context )
2012-08-07 18:04:12 +00:00
return user_ids [ 0 ] if user_ids else uid
2012-06-14 14:17:32 +00:00
2012-08-07 18:04:12 +00:00
def message_route ( self , cr , uid , message , model = None , thread_id = None ,
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
: 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 .
2012-08-10 13:19:19 +00:00
: return : list of [ model , thread_id , custom_values , user_id ]
2012-08-07 18:04:12 +00:00
"""
2012-08-09 17:16:55 +00:00
assert isinstance ( message , Message ) , ' message must be an email.message.Message at this point '
message_id = message . get ( ' Message-Id ' )
2012-08-07 18:04:12 +00:00
# 1. Verify if this is a reply to an existing thread
2012-08-10 13:19:19 +00:00
references = decode_header ( message , ' References ' ) or decode_header ( message , ' In-Reply-To ' )
2012-08-09 17:16:55 +00:00
ref_match = references and tools . reference_re . search ( references )
2012-08-07 18:04:12 +00:00
if ref_match :
thread_id = int ( ref_match . group ( 1 ) )
model = ref_match . group ( 2 ) or model
model_pool = self . pool . get ( model )
if thread_id and model and model_pool and model_pool . exists ( cr , uid , thread_id ) \
and hasattr ( model_pool , ' message_update ' ) :
2012-08-14 08:04:21 +00:00
_logger . debug ( ' Routing mail with Message-Id %s : direct reply to model: %s , thread_id: %s , custom_values: %s , uid: %s ' ,
2012-08-09 17:16:55 +00:00
message_id , model , thread_id , custom_values , uid )
2012-08-10 13:19:19 +00:00
return [ ( model , thread_id , custom_values , uid ) ]
2012-08-15 13:36:43 +00:00
2012-08-07 18:04:12 +00:00
# 2. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
2012-08-10 13:19:19 +00:00
rcpt_tos = decode_header ( message , ' Delivered-To ' ) or \
2012-08-14 08:04:21 +00:00
' , ' . join ( [ 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 ) ]
2012-08-07 18:04:12 +00:00
if local_parts :
mail_alias = self . pool . get ( ' mail.alias ' )
alias_ids = mail_alias . search ( cr , uid , [ ( ' alias_name ' , ' in ' , local_parts ) ] )
2012-08-09 17:16:55 +00:00
if alias_ids :
2012-08-10 13:19:19 +00:00
routes = [ ]
for alias in mail_alias . browse ( cr , uid , alias_ids , context = context ) :
user_id = alias . alias_user_id . id
if not user_id :
user_id = self . _message_find_user_id ( cr , uid , message , context = context )
routes . append ( ( alias . alias_model_id . model , alias . alias_force_thread_id , \
eval ( alias . alias_defaults ) , user_id ) )
_logger . debug ( ' Routing mail with Message-Id %s : direct alias match: %r ' , message_id , routes )
return routes
2012-08-15 13:36:43 +00:00
2012-08-07 18:04:12 +00:00
# 3. Fallback to the provided parameters, if they work
model_pool = self . pool . get ( model )
2012-08-10 13:19:19 +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 )
2012-08-07 18:04:12 +00:00
assert thread_id and hasattr ( model_pool , ' message_update ' ) or hasattr ( model_pool , ' message_new ' ) , \
" No possible route found for incoming message with Message-Id %s . " \
2012-08-14 08:04:21 +00:00
" Create an appropriate mail.alias or force the destination model. " % message_id
2012-08-10 13:19:19 +00:00
if thread_id and not model_pool . exists ( cr , uid , thread_id ) :
2012-08-09 17:16:55 +00:00
_logger . warning ( ' Received mail reply to missing document %s ! Ignoring and creating new document instead for Message-Id %s ' ,
thread_id , message_id )
thread_id = None
_logger . debug ( ' Routing mail with Message-Id %s : fallback to model: %s , thread_id: %s , custom_values: %s , uid: %s ' ,
message_id , model , thread_id , custom_values , uid )
2012-08-10 13:19:19 +00:00
return [ ( model , thread_id , custom_values , uid ) ]
2012-08-07 18:04:12 +00:00
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-08-07 18:04:12 +00:00
""" Process an incoming RFC2822 email message, relying on
` ` mail . message . parse ( ) ` ` for the parsing operation ,
2012-08-15 13:36:43 +00:00
and ` ` message_route ( ) ` ` to figure out the target model .
2012-08-07 18:04:12 +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
: 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-08-07 18:04:12 +00:00
if context is None : context = { }
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 )
2012-08-10 13:19:19 +00:00
routes = self . message_route ( cr , uid , msg_txt , model ,
thread_id , custom_values ,
context = context )
2012-08-23 18:54:43 +00:00
msg = self . message_parse ( cr , uid , msg_txt , save_original = save_original , context = context )
if strip_attachments : msg . pop ( ' attachments ' , None )
2012-09-16 15:10:38 +00:00
thread_id = False
2012-08-15 13:36:43 +00:00
for model , thread_id , custom_values , user_id in routes :
2012-08-10 13:19:19 +00:00
if self . _name != model :
context . update ( { ' thread_model ' : model } )
model_pool = self . pool . get ( 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 " % \
( msg [ ' message-id ' ] , model )
if thread_id and hasattr ( model_pool , ' message_update ' ) :
model_pool . message_update ( cr , user_id , [ thread_id ] , msg , context = context )
2011-07-22 16:34:57 +00:00
else :
2012-08-10 13:19:19 +00:00
thread_id = model_pool . message_new ( cr , user_id , msg , custom_values , context = context )
2012-08-22 12:38:58 +00:00
self . message_post ( cr , uid , [ thread_id ] , context = context , * * msg )
2012-09-13 07:17:24 +00:00
return thread_id
2011-07-22 16:34:57 +00:00
2012-02-01 16:21:36 +00:00
def message_new ( self , cr , uid , msg_dict , custom_values = None , context = None ) :
""" 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
2012-02-01 16:21:36 +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 ) .
2012-02-01 16:21:36 +00:00
Additional behavior may be implemented by overriding this method .
: param dict msg_dict : a map containing the email details and
attachments . See ` ` message_process ` ` and
` ` mail . message . parse ` ` for details .
: param dict custom_values : optional dictionary of additional
field values to pass to create ( )
when creating the new thread record .
Be careful , these values may override
any other values coming from the message .
: param dict context : if a ` ` thread_model ` ` value is present
in the context , its value will be used
to determine the model of the record
to create ( instead of the current model ) .
: rtype : int
: return : the id of the newly created thread object
"""
if context is None :
context = { }
model = context . get ( ' thread_model ' ) or self . _name
model_pool = self . pool . get ( model )
fields = model_pool . fields_get ( cr , uid , context = context )
data = model_pool . default_get ( cr , uid , fields , 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 ' , ' ' )
2012-02-01 16:21:36 +00:00
if custom_values and isinstance ( custom_values , dict ) :
data . update ( custom_values )
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 ) :
2012-02-01 16:21:36 +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 .
2012-02-01 16:21:36 +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 .
2012-02-01 16:21:36 +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/>
body = tools . append_content_to_html ( u ' ' , body )
2012-08-23 18:54:43 +00:00
else :
alternative = ( message . get_content_type ( ) == ' multipart/alternative ' )
for part in message . walk ( ) :
if part . get_content_maintype ( ) == ' multipart ' :
continue # skip container
filename = part . get_filename ( ) # None if normal part
encoding = part . get_content_charset ( ) # None if attachment
# 1) Explicit Attachments -> attachments
2012-09-05 15:51:21 +00:00
if filename or part . get ( ' content-disposition ' , ' ' ) . strip ( ) . startswith ( ' attachment ' ) :
2012-08-23 18:54:43 +00:00
attachments . append ( ( filename or ' attachment ' , part . get_payload ( decode = True ) ) )
continue
# 2) text/plain -> <pre/>
if part . get_content_type ( ) == ' text/plain ' and ( not alternative or not body ) :
2012-08-31 15:51:03 +00:00
body = tools . append_content_to_html ( body , tools . ustr ( part . get_payload ( decode = True ) ,
encoding , errors = ' replace ' ) )
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
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
2012-08-16 15:48:23 +00:00
: rtype : dict
: return : A dict with the following structure , where each
field may not be present if missing in original
message : :
{ ' message-id ' : msg_id ,
' subject ' : subject ,
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-08-23 18:54:43 +00:00
msg_dict = { }
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
2012-08-23 18:54:43 +00:00
if ' Subject ' in message :
msg_dict [ ' subject ' ] = decode ( message . get ( ' Subject ' ) )
2012-08-16 15:48:23 +00:00
2012-09-05 15:51:21 +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 ' ) )
2012-08-16 15:48:23 +00:00
2012-08-23 18:54:43 +00:00
if ' From ' in message :
author_ids = self . _message_find_partners ( cr , uid , message , [ ' From ' ] , context = context )
2012-08-16 15:48:23 +00:00
if author_ids :
2012-08-23 18:54:43 +00:00
msg_dict [ ' author_id ' ] = author_ids [ 0 ]
2012-09-05 15:51:21 +00:00
partner_ids = self . _message_find_partners ( cr , uid , message , [ ' From ' , ' To ' , ' Cc ' ] , context = context )
2012-08-23 18:54:43 +00:00
msg_dict [ ' partner_ids ' ] = partner_ids
2012-08-16 15:48:23 +00:00
2012-08-23 18:54:43 +00:00
if ' Date ' in message :
date_hdr = decode ( message . get ( ' Date ' ) )
2012-08-22 11:34:39 +00:00
# convert from email timezone to server timezone
date_server_datetime = dateutil . parser . parse ( date_hdr ) . astimezone ( pytz . timezone ( tools . get_server_timezone ( ) ) )
date_server_datetime_str = date_server_datetime . strftime ( tools . DEFAULT_SERVER_DATETIME_FORMAT )
2012-08-23 18:54:43 +00:00
msg_dict [ ' date ' ] = date_server_datetime_str
2012-08-16 15:48:23 +00:00
2012-08-23 18:54:43 +00:00
if ' In-Reply-To ' in message :
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 ]
if ' References ' in message 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
2012-08-23 18:54:43 +00:00
msg_dict [ ' body ' ] , msg_dict [ ' attachments ' ] = self . _message_extract_payload ( message )
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 )
2012-09-19 11:43:53 +00:00
def message_post ( self , cr , uid , thread_id , body = ' ' , subject = False , type = ' notification ' ,
2012-09-20 10:17:04 +00:00
subtype = None , parent_id = False , attachments = None , context = None , * * kwargs ) :
2012-09-04 13:36:48 +00:00
""" Post a new message in an existing thread, returning the new
2012-08-31 08:01:03 +00:00
mail . message ID . Extra keyword arguments will be used as default
column values for the new mail . message record .
: param int thread_id : thread ID to post into , or list with one ID
: param str body : body of the message , usually raw HTML that will
be sanitized
: param str subject : optional subject
2012-09-04 13:36:48 +00:00
: param str type : mail_message . type
2012-08-31 08:01:03 +00:00
: param int parent_id : optional ID of parent message in this thread
: param tuple ( str , str ) attachments : list of attachment tuples in the form
` ` ( name , content ) ` ` , where content is NOT base64 encoded
2012-09-05 15:51:21 +00:00
: return : ID of newly created mail . message
2012-08-22 11:34:39 +00:00
"""
2012-08-17 10:03:02 +00:00
context = context or { }
2012-08-31 08:01:03 +00:00
attachments = attachments or [ ]
2012-09-20 10:17:04 +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 "
2012-08-22 11:34:39 +00:00
if isinstance ( thread_id , ( list , tuple ) ) :
thread_id = thread_id and thread_id [ 0 ]
2012-08-17 10:03:02 +00:00
2012-08-31 08:01:03 +00:00
attachment_ids = [ ]
2012-08-22 11:34:39 +00:00
for name , content in attachments :
if isinstance ( content , unicode ) :
content = content . encode ( ' utf-8 ' )
2012-08-17 10:03:02 +00:00
data_attach = {
2012-08-22 11:34:39 +00:00
' name ' : name ,
2012-08-23 18:54:43 +00:00
' datas ' : base64 . b64encode ( str ( content ) ) ,
2012-08-22 11:34:39 +00:00
' datas_fname ' : name ,
' description ' : name ,
2012-08-23 18:54:43 +00:00
' res_model ' : context . get ( ' thread_model ' ) or self . _name ,
' res_id ' : thread_id ,
2012-08-17 10:03:02 +00:00
}
2012-09-05 15:51:21 +00:00
attachment_ids . append ( ( 0 , 0 , data_attach ) )
2012-08-17 10:03:02 +00:00
2012-09-20 10:17:04 +00:00
if subtype :
ref = self . pool . get ( ' ir.model.data ' ) . get_object_reference ( cr , uid , ' mail ' , subtype )
subtype_id = ref and ref [ 1 ] or False
else :
subtype_id = False
2012-08-22 11:34:39 +00:00
values = kwargs
2012-09-05 15:51:21 +00:00
values . update ( {
2012-09-04 15:50:23 +00:00
' model ' : context . get ( ' thread_model ' , self . _name ) if thread_id else False ,
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-08-17 10:26:50 +00:00
' subject ' : subject ,
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 ,
2012-08-17 10:03:02 +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-08-22 11:34:39 +00:00
return self . pool . get ( ' mail.message ' ) . create ( cr , uid , values , context = context )
2012-04-25 05:41:43 +00:00
2012-02-01 16:21:36 +00:00
#------------------------------------------------------
2012-08-22 11:03:13 +00:00
# Followers API
2012-02-01 16:21:36 +00:00
#------------------------------------------------------
2012-04-25 05:41:43 +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-09-20 10:17:04 +00:00
if not user_ids :
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. """
2012-09-20 10:17:04 +00:00
self . write ( cr , uid , ids , { ' message_follower_ids ' : [ ( 4 , pid ) for pid in partner_ids ] } , context = context )
2012-08-29 06:55:14 +00:00
if not subtype_ids :
subtype_obj = self . pool . get ( ' mail.message.subtype ' )
2012-09-20 10:17:04 +00:00
subtype_ids = subtype_obj . search ( cr , uid , [ ( ' default ' , ' = ' , True ) , ' | ' , ( ' res_model ' , ' = ' , self . _name ) , ( ' res_model ' , ' = ' , False ) ] , context = context )
fol_obj = self . pool . get ( ' mail.followers ' )
fol_ids = fol_obj . search ( cr , uid , [ ( ' res_model ' , ' = ' , self . _name ) , ( ' res_id ' , ' in ' , ids ) , ( ' partner_id ' , ' in ' , partner_ids ) ] , context = context )
fol_obj . write ( cr , uid , fol_ids , { ' subtype_ids ' : [ ( 6 , 0 , subtype_ids ) ] } , context = context )
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-09-20 10:17:04 +00:00
if not user_ids :
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-04-25 05:41: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. """
return self . write ( cr , uid , ids , { ' message_follower_ids ' : [ ( 3 , pid ) for pid in partner_ids ] } , context = context )
2012-03-21 17:20:18 +00:00
2012-06-04 09:33:24 +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-06-04 09:33:24 +00:00
def message_mark_as_unread ( self , cr , uid , ids , context = None ) :
2012-07-02 15:46:30 +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
2012-08-13 11:59:03 +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
2011-07-22 16:34:57 +00:00
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: