2012-08-22 07:45:45 +00:00
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
#
# 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/>
#
##############################################################################
2012-08-15 18:44:03 +00:00
import base64
import logging
2013-03-21 09:23:32 +00:00
import re
2014-08-12 11:40:45 +00:00
from email . utils import formataddr
2013-01-04 12:57:54 +00:00
from urllib import urlencode
from urlparse import urljoin
2012-08-15 18:44:03 +00:00
2015-09-25 12:43:00 +00:00
import psycopg2
2013-01-04 12:57:54 +00:00
from openerp import tools
2012-09-14 11:51:07 +00:00
from openerp import SUPERUSER_ID
2013-06-04 15:15:08 +00:00
from openerp . addons . base . ir . ir_mail_server import MailDeliveryException
2012-12-06 14:56:32 +00:00
from openerp . osv import fields , osv
from openerp . tools . translate import _
2012-08-15 18:44:03 +00:00
2012-08-22 01:09:47 +00:00
_logger = logging . getLogger ( __name__ )
2012-09-12 12:01:44 +00:00
2012-08-15 17:08:22 +00:00
class mail_mail ( osv . Model ) :
2012-08-31 09:31:39 +00:00
""" Model holding RFC2822 email messages to send. This model also provides
facilities to queue and send new email messages . """
2012-08-15 17:08:22 +00:00
_name = ' mail.mail '
_description = ' Outgoing Mails '
2012-08-16 09:26:16 +00:00
_inherits = { ' mail.message ' : ' mail_message_id ' }
2012-08-30 08:51:16 +00:00
_order = ' id desc '
2012-08-15 17:08:22 +00:00
_columns = {
2012-08-16 09:26:16 +00:00
' mail_message_id ' : fields . many2one ( ' mail.message ' , ' Message ' , required = True , ondelete = ' cascade ' ) ,
2012-08-15 17:08:22 +00:00
' state ' : fields . selection ( [
2012-08-15 19:34:06 +00:00
( ' outgoing ' , ' Outgoing ' ) ,
( ' sent ' , ' Sent ' ) ,
( ' received ' , ' Received ' ) ,
( ' exception ' , ' Delivery Failed ' ) ,
( ' cancel ' , ' Cancelled ' ) ,
] , ' Status ' , readonly = True ) ,
2012-08-15 17:08:22 +00:00
' auto_delete ' : fields . boolean ( ' Auto Delete ' ,
help = " Permanently delete this email after sending it, to save space " ) ,
2012-08-16 09:26:16 +00:00
' references ' : fields . text ( ' References ' , help = ' Message references, such as identifiers of previous messages ' , readonly = 1 ) ,
2013-02-25 16:48:57 +00:00
' email_to ' : fields . text ( ' To ' , help = ' Message recipients (emails) ' ) ,
2013-03-14 12:06:11 +00:00
' recipient_ids ' : fields . many2many ( ' res.partner ' , string = ' To (Partners) ' ) ,
2012-08-30 17:44:52 +00:00
' email_cc ' : fields . char ( ' Cc ' , help = ' Carbon copy message recipients ' ) ,
2012-08-30 08:51:16 +00:00
' body_html ' : fields . text ( ' Rich-text Contents ' , help = " Rich-text/HTML message " ) ,
2012-09-05 15:19:50 +00:00
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
2012-09-12 12:01:44 +00:00
# and during unlink() we will not cascade delete the parent and its attachments
2013-03-19 09:18:44 +00:00
' notification ' : fields . boolean ( ' Is Notification ' ,
2013-08-06 15:11:43 +00:00
help = ' Mail has been created to notify people of an existing mail.message ' ) ,
2012-08-15 17:08:22 +00:00
}
_defaults = {
2012-08-15 18:44:03 +00:00
' state ' : ' outgoing ' ,
2012-08-15 17:08:22 +00:00
}
2013-02-01 09:13:03 +00:00
def default_get ( self , cr , uid , fields , context = None ) :
# protection for `default_type` values leaking from menu action context (e.g. for invoices)
# To remove when automatic context propagation is removed in web client
if context and context . get ( ' default_type ' ) and context . get ( ' default_type ' ) not in self . _all_columns [ ' type ' ] . column . selection :
2013-02-25 16:48:57 +00:00
context = dict ( context , default_type = None )
2013-02-01 09:13:03 +00:00
return super ( mail_mail , self ) . default_get ( cr , uid , fields , context = context )
2012-09-05 15:19:50 +00:00
def create ( self , cr , uid , values , context = None ) :
2013-03-18 15:02:52 +00:00
# notification field: if not set, set if mail comes from an existing mail.message
2012-09-05 15:19:50 +00:00
if ' notification ' not in values and values . get ( ' mail_message_id ' ) :
values [ ' notification ' ] = True
2013-08-27 13:30:58 +00:00
return super ( mail_mail , self ) . create ( cr , uid , values , context = context )
2012-09-05 15:19:50 +00:00
def unlink ( self , cr , uid , ids , context = None ) :
# cascade-delete the parent message for all mails that are not created for a notification
2012-09-12 12:01:44 +00:00
ids_to_cascade = self . search ( cr , uid , [ ( ' notification ' , ' = ' , False ) , ( ' id ' , ' in ' , ids ) ] )
2012-09-05 15:19:50 +00:00
parent_msg_ids = [ m . mail_message_id . id for m in self . browse ( cr , uid , ids_to_cascade , context = context ) ]
2012-09-12 12:01:44 +00:00
res = super ( mail_mail , self ) . unlink ( cr , uid , ids , context = context )
2012-09-05 15:19:50 +00:00
self . pool . get ( ' mail.message ' ) . unlink ( cr , uid , parent_msg_ids , context = context )
return res
2012-08-15 17:08:22 +00:00
def mark_outgoing ( self , cr , uid , ids , context = None ) :
2012-09-12 12:01:44 +00:00
return self . write ( cr , uid , ids , { ' state ' : ' outgoing ' } , context = context )
2012-08-15 17:08:22 +00:00
def cancel ( self , cr , uid , ids , context = None ) :
2012-09-12 12:01:44 +00:00
return self . write ( cr , uid , ids , { ' state ' : ' cancel ' } , context = context )
2012-08-15 17:08:22 +00:00
def process_email_queue ( self , cr , uid , ids = None , context = None ) :
""" Send immediately queued messages, committing after each
message is sent - this is not transactional and should
not be called during another transaction !
: param list ids : optional list of emails ids to send . If passed
no search is performed , and these ids are used
instead .
: param dict context : if a ' filters ' key is present in context ,
this value will be used as an additional
filter to further restrict the outgoing
messages to send ( by default all ' outgoing '
messages are sent ) .
"""
if context is None :
context = { }
if not ids :
2013-06-14 13:24:18 +00:00
filters = [ ( ' state ' , ' = ' , ' outgoing ' ) ]
2012-08-15 17:08:22 +00:00
if ' filters ' in context :
filters . extend ( context [ ' filters ' ] )
ids = self . search ( cr , uid , filters , context = context )
res = None
try :
# Force auto-commit - this is meant to be called by
# the scheduler, and we can't allow rolling back the status
# of previously sent emails!
res = self . send ( cr , uid , ids , auto_commit = True , context = context )
except Exception :
_logger . exception ( " Failed processing mail queue " )
return res
2012-09-05 15:19:50 +00:00
def _postprocess_sent_message ( self , cr , uid , mail , context = None ) :
""" Perform any post-processing necessary after sending ``mail``
2012-08-15 17:08:22 +00:00
successfully , including deleting it completely along with its
2012-09-05 15:19:50 +00:00
attachment if the ` ` auto_delete ` ` flag of the mail was set .
2012-09-12 12:01:44 +00:00
Overridden by subclasses for extra post - processing behaviors .
2012-08-15 17:08:22 +00:00
2012-09-05 15:19:50 +00:00
: param browse_record mail : the mail that was just sent
2012-08-15 17:08:22 +00:00
: return : True
"""
2012-09-05 15:19:50 +00:00
if mail . auto_delete :
2012-09-14 11:51:07 +00:00
# done with SUPERUSER_ID to avoid giving large unlink access rights
self . unlink ( cr , SUPERUSER_ID , [ mail . id ] , context = context )
2012-08-15 17:08:22 +00:00
return True
2013-04-17 12:22:25 +00:00
#------------------------------------------------------
# mail_mail formatting, tools and send mechanism
#------------------------------------------------------
def _get_partner_access_link ( self , cr , uid , mail , partner = None , context = None ) :
""" Generate URLs for links in mails:
- partner is an user and has read access to the document : direct link to document with model , res_id
"""
if partner and partner . user_ids :
2014-12-15 16:34:21 +00:00
base_url = self . pool . get ( ' ir.config_parameter ' ) . get_param ( cr , SUPERUSER_ID , ' web.base.url ' )
2013-04-17 12:22:25 +00:00
# the parameters to encode for the query and fragment part of url
query = { ' db ' : cr . dbname }
fragment = {
' login ' : partner . user_ids [ 0 ] . login ,
' action ' : ' mail.action_mail_redirect ' ,
}
if mail . notification :
2013-10-18 14:49:24 +00:00
fragment [ ' message_id ' ] = mail . mail_message_id . id
elif mail . model and mail . res_id :
fragment . update ( model = mail . model , res_id = mail . res_id )
2014-02-25 09:50:54 +00:00
2014-02-26 17:00:02 +00:00
url = urljoin ( base_url , " /web? %s # %s " % ( urlencode ( query ) , urlencode ( fragment ) ) )
2013-12-19 13:31:44 +00:00
return _ ( """ <span class= ' oe_mail_footer_access ' ><small>Access your messages and documents <a style= ' color:inherit ' href= " %s " >in OpenERP</a></small></span> """ ) % url
2013-04-17 12:22:25 +00:00
else :
return None
2012-09-12 15:53:00 +00:00
def send_get_mail_subject ( self , cr , uid , mail , force = False , partner = None , context = None ) :
2012-09-13 15:48:44 +00:00
""" If subject is void and record_name defined: ' <Author> posted on <Resource> '
2012-09-12 13:38:43 +00:00
2012-09-13 15:48:44 +00:00
: param boolean force : force the subject replacement
: param browse_record mail : mail . mail browse_record
: param browse_record partner : specific recipient partner
2012-09-12 13:38:43 +00:00
"""
2013-05-15 13:33:32 +00:00
if ( force or not mail . subject ) and mail . record_name :
2013-02-14 12:39:25 +00:00
return ' Re: %s ' % ( mail . record_name )
2013-05-15 13:33:32 +00:00
elif ( force or not mail . subject ) and mail . parent_id and mail . parent_id . subject :
return ' Re: %s ' % ( mail . parent_id . subject )
2012-08-31 09:31:39 +00:00
return mail . subject
2013-03-15 11:04:21 +00:00
def send_get_mail_body ( self , cr , uid , mail , partner = None , context = None ) :
""" Return a specific ir_email body. The main purpose of this method
is to be inherited to add custom content depending on some module .
: param browse_record mail : mail . mail browse_record
: param browse_record partner : specific recipient partner
"""
body = mail . body_html
2013-04-17 12:22:25 +00:00
# generate footer
link = self . _get_partner_access_link ( cr , uid , mail , partner , context = context )
if link :
body = tools . append_content_to_html ( body , link , plaintext = False , container_tag = ' div ' )
2013-01-04 12:57:54 +00:00
return body
2012-09-12 13:38:43 +00:00
2012-09-13 15:48:44 +00:00
def send_get_email_dict ( self , cr , uid , mail , partner = None , context = None ) :
""" Return a dictionary for specific email values, depending on a
2012-09-12 13:38:43 +00:00
partner , or generic to the whole recipients given by mail . email_to .
2012-09-13 15:48:44 +00:00
: param browse_record mail : mail . mail browse_record
: param browse_record partner : specific recipient partner
2012-09-12 13:38:43 +00:00
"""
2012-09-12 15:53:00 +00:00
body = self . send_get_mail_body ( cr , uid , mail , partner = partner , context = context )
subject = self . send_get_mail_subject ( cr , uid , mail , partner = partner , context = context )
2012-09-12 13:38:43 +00:00
body_alternative = tools . html2plaintext ( body )
2013-04-03 12:10:22 +00:00
# generate email_to, heuristic:
# 1. if 'partner' is specified and there is a related document: Followers of 'Doc' <email>
# 2. if 'partner' is specified, but no related document: Partner Name <email>
# 3; fallback on mail.email_to that we split to have an email addresses list
if partner and mail . record_name :
2014-08-12 11:40:45 +00:00
email_to = [ formataddr ( ( _ ( ' Followers of %s ' ) % mail . record_name , partner . email ) ) ]
2013-04-03 12:10:22 +00:00
elif partner :
2014-08-12 11:40:45 +00:00
email_to = [ formataddr ( ( partner . name , partner . email ) ) ]
2013-04-03 12:10:22 +00:00
else :
email_to = tools . email_split ( mail . email_to )
2012-09-12 13:38:43 +00:00
return {
' body ' : body ,
' body_alternative ' : body_alternative ,
' subject ' : subject ,
' email_to ' : email_to ,
}
2013-05-24 11:48:57 +00:00
def send ( self , cr , uid , ids , auto_commit = False , raise_exception = False , context = None ) :
2012-08-31 09:31:39 +00:00
""" Sends the selected emails immediately, ignoring their current
state ( mails that have already been sent should not be passed
unless they should actually be re - sent ) .
Emails successfully delivered are marked as ' sent ' , and those
that fail to be deliver are marked as ' exception ' , and the
corresponding error mail is output in the server logs .
: param bool auto_commit : whether to force a commit of the mail status
after sending each mail ( meant only for scheduler processing ) ;
should never be True during normal transactions ( default : False )
2013-05-24 11:48:57 +00:00
: param bool raise_exception : whether to raise an exception if the
2013-06-04 15:15:08 +00:00
email sending process has failed
2012-08-31 09:31:39 +00:00
: return : True
2012-08-15 17:08:22 +00:00
"""
ir_mail_server = self . pool . get ( ' ir.mail_server ' )
2014-05-02 14:44:31 +00:00
ir_attachment = self . pool [ ' ir.attachment ' ]
2014-05-05 12:38:52 +00:00
2013-02-25 16:48:57 +00:00
for mail in self . browse ( cr , SUPERUSER_ID , ids , context = context ) :
2012-08-15 17:08:22 +00:00
try :
2014-05-02 14:44:31 +00:00
# load attachment binary data with a separate read(), as prefetching all
# `datas` (binary field) could bloat the browse cache, triggerring
# soft/hard mem limits with temporary data.
attachment_ids = [ a . id for a in mail . attachment_ids ]
2014-05-05 12:38:52 +00:00
attachments = [ ( a [ ' datas_fname ' ] , base64 . b64decode ( a [ ' datas ' ] ) )
for a in ir_attachment . read ( cr , SUPERUSER_ID , attachment_ids ,
[ ' datas_fname ' , ' datas ' ] ) ]
2012-09-12 13:38:43 +00:00
# specific behavior to customize the send email for notified partners
2012-09-13 15:48:44 +00:00
email_list = [ ]
2013-02-25 16:48:57 +00:00
if mail . email_to :
2012-09-13 15:48:44 +00:00
email_list . append ( self . send_get_email_dict ( cr , uid , mail , context = context ) )
2013-02-25 16:48:57 +00:00
for partner in mail . recipient_ids :
email_list . append ( self . send_get_email_dict ( cr , uid , mail , partner = partner , context = context ) )
2013-08-06 15:11:43 +00:00
# headers
headers = { }
bounce_alias = self . pool [ ' ir.config_parameter ' ] . get_param ( cr , uid , " mail.bounce.alias " , context = context )
catchall_domain = self . pool [ ' ir.config_parameter ' ] . get_param ( cr , uid , " mail.catchall.domain " , context = context )
if bounce_alias and catchall_domain :
if mail . model and mail . res_id :
headers [ ' Return-Path ' ] = ' %s - %d - %s - %d @ %s ' % ( bounce_alias , mail . id , mail . model , mail . res_id , catchall_domain )
else :
headers [ ' Return-Path ' ] = ' %s - %d @ %s ' % ( bounce_alias , mail . id , catchall_domain )
2012-08-15 17:08:22 +00:00
2012-08-31 09:31:39 +00:00
# build an RFC2822 email.message.Message object and send it without queuing
2013-08-06 15:11:43 +00:00
res = None
2012-09-13 15:48:44 +00:00
for email in email_list :
2012-09-12 13:38:43 +00:00
msg = ir_mail_server . build_email (
2013-08-06 15:11:43 +00:00
email_from = mail . email_from ,
email_to = email . get ( ' email_to ' ) ,
subject = email . get ( ' subject ' ) ,
body = email . get ( ' body ' ) ,
body_alternative = email . get ( ' body_alternative ' ) ,
email_cc = tools . email_split ( mail . email_cc ) ,
reply_to = mail . reply_to ,
attachments = attachments ,
message_id = mail . message_id ,
references = mail . references ,
object_id = mail . res_id and ( ' %s - %s ' % ( mail . res_id , mail . model ) ) ,
subtype = ' html ' ,
subtype_alternative = ' plain ' ,
headers = headers )
2014-10-03 13:19:30 +00:00
try :
2014-11-04 16:54:48 +00:00
res = ir_mail_server . send_email ( cr , uid , msg ,
2013-08-06 15:11:43 +00:00
mail_server_id = mail . mail_server_id . id ,
context = context )
2014-10-03 13:19:30 +00:00
except AssertionError as error :
if error . message == ir_mail_server . NO_VALID_RECIPIENT :
# No valid recipient found for this particular
# mail item -> ignore error to avoid blocking
# delivery to next recipients, if any. If this is
# the only recipient, the mail will show as failed.
_logger . warning ( " Ignoring invalid recipients for mail.mail %s : %s " ,
mail . message_id , email . get ( ' email_to ' ) )
else :
raise
2012-08-15 17:08:22 +00:00
if res :
2012-09-12 12:01:44 +00:00
mail . write ( { ' state ' : ' sent ' , ' message_id ' : res } )
2012-12-18 02:11:23 +00:00
mail_sent = True
2012-08-15 17:08:22 +00:00
else :
2012-09-12 12:01:44 +00:00
mail . write ( { ' state ' : ' exception ' } )
2012-12-18 02:11:23 +00:00
mail_sent = False
# /!\ can't use mail.state here, as mail.refresh() will cause an error
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
if mail_sent :
2014-05-02 14:44:31 +00:00
_logger . info ( ' Mail with ID %r and Message-Id %r successfully sent ' , mail . id , mail . message_id )
2012-08-31 09:31:39 +00:00
self . _postprocess_sent_message ( cr , uid , mail , context = context )
2014-04-30 10:25:52 +00:00
except MemoryError :
# prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
# instead of marking the mail as failed
2014-05-02 14:44:31 +00:00
_logger . exception ( ' MemoryError while processing mail with ID %r and Msg-Id %r . ' \
' Consider raising the --limit-memory-hard startup option ' ,
mail . id , mail . message_id )
2014-04-30 10:25:52 +00:00
raise
2015-09-25 12:43:00 +00:00
except psycopg2 . Error :
# If an error with the database occurs, chances are that the cursor is unusable.
# This will lead to an `psycopg2.InternalError` being raised when trying to write
# `state`, shadowing the original exception and forbid a retry on concurrent
# update. Let's bubble it.
raise
2013-06-04 15:15:08 +00:00
except Exception as e :
_logger . exception ( ' failed sending mail.mail %s ' , mail . id )
mail . write ( { ' state ' : ' exception ' } )
2013-05-24 11:48:57 +00:00
if raise_exception :
2013-06-04 15:15:08 +00:00
if isinstance ( e , AssertionError ) :
# get the args of the original error, wrap into a value and throw a MailDeliveryException
# that is an except_orm, with name and value as arguments
value = ' . ' . join ( e . args )
raise MailDeliveryException ( _ ( " Mail Delivery Failed " ) , value )
2013-05-24 11:48:57 +00:00
raise
2012-08-15 17:08:22 +00:00
if auto_commit == True :
cr . commit ( )
return True