[IMP] mail: first implementation of tracking and bounce management.
Added 2 fields on mail_mail to count read/bounce. Added bounce alias bounce-mail_id-model-res_id. Added message_receive_bounce method that try to incremetn message_bounce field. bzr revid: tde@openerp.com-20130806151143-7dw6xlj8n7mh0nqe
This commit is contained in:
parent
ade561e1cc
commit
38a534dee0
|
@ -273,6 +273,8 @@ class crm_lead(format_address, osv.osv):
|
||||||
selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
|
selection=crm.AVAILABLE_STATES, string="Status", readonly=True,
|
||||||
help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'),
|
help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'),
|
||||||
|
|
||||||
|
# Messaging and marketing
|
||||||
|
'message_bounce': fields.integer('Bounce'),
|
||||||
# Only used for type opportunity
|
# Only used for type opportunity
|
||||||
'probability': fields.float('Success Rate (%)',group_operator="avg"),
|
'probability': fields.float('Success Rate (%)',group_operator="avg"),
|
||||||
'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
|
'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),
|
||||||
|
|
|
@ -182,6 +182,7 @@
|
||||||
</group>
|
</group>
|
||||||
<group string="Mailings">
|
<group string="Mailings">
|
||||||
<field name="opt_out"/>
|
<field name="opt_out"/>
|
||||||
|
<field name="message_bounce"/>
|
||||||
</group>
|
</group>
|
||||||
<group string="Misc">
|
<group string="Misc">
|
||||||
<field name="active"/>
|
<field name="active"/>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import base64
|
import base64
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
from openerp import SUPERUSER_ID
|
from openerp import SUPERUSER_ID
|
||||||
import openerp.addons.web.http as oeweb
|
import openerp.addons.web.http as http
|
||||||
from openerp.addons.web.controllers.main import content_disposition
|
from openerp.addons.web.controllers.main import content_disposition
|
||||||
|
from openerp.addons.web.http import request
|
||||||
|
|
||||||
#----------------------------------------------------------
|
|
||||||
# Controller
|
class MailController(http.Controller):
|
||||||
#----------------------------------------------------------
|
|
||||||
class MailController(oeweb.Controller):
|
|
||||||
_cp_path = '/mail'
|
_cp_path = '/mail'
|
||||||
|
|
||||||
@oeweb.httprequest
|
@http.httprequest
|
||||||
def download_attachment(self, req, model, id, method, attachment_id, **kw):
|
def download_attachment(self, req, model, id, method, attachment_id, **kw):
|
||||||
Model = req.session.model(model)
|
Model = req.session.model(model)
|
||||||
res = getattr(Model, method)(int(id), int(attachment_id))
|
res = getattr(Model, method)(int(id), int(attachment_id))
|
||||||
|
@ -24,7 +24,7 @@ class MailController(oeweb.Controller):
|
||||||
('Content-Disposition', content_disposition(filename, req))])
|
('Content-Disposition', content_disposition(filename, req))])
|
||||||
return req.not_found()
|
return req.not_found()
|
||||||
|
|
||||||
@oeweb.jsonrequest
|
@http.jsonrequest
|
||||||
def receive(self, req):
|
def receive(self, req):
|
||||||
""" End-point to receive mail from an external SMTP server. """
|
""" End-point to receive mail from an external SMTP server. """
|
||||||
dbs = req.jsonrequest.get('databases')
|
dbs = req.jsonrequest.get('databases')
|
||||||
|
@ -38,3 +38,10 @@ class MailController(oeweb.Controller):
|
||||||
except psycopg2.Error:
|
except psycopg2.Error:
|
||||||
pass
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='admin')
|
||||||
|
def track_read_email(self, mail_id):
|
||||||
|
""" Email tracking. """
|
||||||
|
mail_mail = request.registry.get('mail.mail')
|
||||||
|
mail_mail.set_opened(request.cr, request.uid, [mail_id])
|
||||||
|
return False
|
||||||
|
|
|
@ -46,6 +46,12 @@
|
||||||
<field name="value">catchall</field>
|
<field name="value">catchall</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- Bounce Email Alias -->
|
||||||
|
<record id="icp_mail_bounce_alias" model="ir.config_parameter">
|
||||||
|
<field name="key">mail.bounce.alias</field>
|
||||||
|
<field name="value">bounce</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<!-- Discussion subtype for messaging / Chatter -->
|
<!-- Discussion subtype for messaging / Chatter -->
|
||||||
<record id="mt_comment" model="mail.message.subtype">
|
<record id="mt_comment" model="mail.message.subtype">
|
||||||
<field name="name">Discussions</field>
|
<field name="name">Discussions</field>
|
||||||
|
|
|
@ -61,15 +61,16 @@ class mail_mail(osv.Model):
|
||||||
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
|
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
|
||||||
# and during unlink() we will not cascade delete the parent and its attachments
|
# and during unlink() we will not cascade delete the parent and its attachments
|
||||||
'notification': fields.boolean('Is Notification',
|
'notification': fields.boolean('Is Notification',
|
||||||
help='Mail has been created to notify people of an existing mail.message')
|
help='Mail has been created to notify people of an existing mail.message'),
|
||||||
|
# Bounce and tracking
|
||||||
|
'opened': fields.integer(
|
||||||
|
'Opened',
|
||||||
|
help='Number of times this email has been seen, using the OpenERP tracking.'),
|
||||||
|
'replied': fields.integer(
|
||||||
|
'Reply Received',
|
||||||
|
help='If checked, a reply to this email has been received.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_default_from(self, cr, uid, context=None):
|
|
||||||
""" Kept for compatibility
|
|
||||||
TDE TODO: remove me in 8.0
|
|
||||||
"""
|
|
||||||
return self.pool['mail.message']._get_default_from(cr, uid, context=context)
|
|
||||||
|
|
||||||
_defaults = {
|
_defaults = {
|
||||||
'state': 'outgoing',
|
'state': 'outgoing',
|
||||||
}
|
}
|
||||||
|
@ -158,6 +159,18 @@ class mail_mail(osv.Model):
|
||||||
def cancel(self, cr, uid, ids, context=None):
|
def cancel(self, cr, uid, ids, context=None):
|
||||||
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
|
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
|
||||||
|
|
||||||
|
def set_opened(self, cr, uid, ids, context=None):
|
||||||
|
""" Increment opened counter """
|
||||||
|
for mail in self.browse(cr, uid, ids, context=context):
|
||||||
|
self.write(cr, uid, [mail.id], {'opened': (mail.opened + 1)}, context=context)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_replied(self, cr, uid, ids, context=None):
|
||||||
|
""" Increment replied counter """
|
||||||
|
for mail in self.browse(cr, uid, ids, context=context):
|
||||||
|
self.write(cr, uid, [mail.id], {'replied': (mail.replied + 1)}, context=context)
|
||||||
|
return True
|
||||||
|
|
||||||
def process_email_queue(self, cr, uid, ids=None, context=None):
|
def process_email_queue(self, cr, uid, ids=None, context=None):
|
||||||
"""Send immediately queued messages, committing after each
|
"""Send immediately queued messages, committing after each
|
||||||
message is sent - this is not transactional and should
|
message is sent - this is not transactional and should
|
||||||
|
@ -226,13 +239,22 @@ class mail_mail(osv.Model):
|
||||||
}
|
}
|
||||||
if mail.notification:
|
if mail.notification:
|
||||||
fragment.update({
|
fragment.update({
|
||||||
'message_id': mail.mail_message_id.id,
|
'message_id': mail.mail_message_id.id,
|
||||||
})
|
})
|
||||||
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
|
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
|
||||||
return _("""<small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small>""") % url
|
return _("""<small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small>""") % url
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
|
||||||
|
if not mail.auto_delete:
|
||||||
|
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
|
||||||
|
track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id)
|
||||||
|
print base_url, track_url
|
||||||
|
return '<img src="%s" alt=""/>' % track_url
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
|
def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
|
||||||
""" If subject is void and record_name defined: '<Author> posted on <Resource>'
|
""" If subject is void and record_name defined: '<Author> posted on <Resource>'
|
||||||
|
|
||||||
|
@ -257,8 +279,11 @@ class mail_mail(osv.Model):
|
||||||
|
|
||||||
# generate footer
|
# generate footer
|
||||||
link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
|
link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
|
||||||
|
tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context)
|
||||||
if link:
|
if link:
|
||||||
body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
|
body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
|
||||||
|
if tracking_url:
|
||||||
|
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
|
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
|
||||||
|
@ -319,25 +344,37 @@ class mail_mail(osv.Model):
|
||||||
email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
|
email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
|
||||||
for partner in mail.recipient_ids:
|
for partner in mail.recipient_ids:
|
||||||
email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
|
email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
|
||||||
|
# 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)
|
||||||
|
|
||||||
# build an RFC2822 email.message.Message object and send it without queuing
|
# build an RFC2822 email.message.Message object and send it without queuing
|
||||||
|
res = None
|
||||||
for email in email_list:
|
for email in email_list:
|
||||||
msg = ir_mail_server.build_email(
|
msg = ir_mail_server.build_email(
|
||||||
email_from = mail.email_from,
|
email_from=mail.email_from,
|
||||||
email_to = email.get('email_to'),
|
email_to=email.get('email_to'),
|
||||||
subject = email.get('subject'),
|
subject=email.get('subject'),
|
||||||
body = email.get('body'),
|
body=email.get('body'),
|
||||||
body_alternative = email.get('body_alternative'),
|
body_alternative=email.get('body_alternative'),
|
||||||
email_cc = tools.email_split(mail.email_cc),
|
email_cc=tools.email_split(mail.email_cc),
|
||||||
reply_to = mail.reply_to,
|
reply_to=mail.reply_to,
|
||||||
attachments = attachments,
|
attachments=attachments,
|
||||||
message_id = mail.message_id,
|
message_id=mail.message_id,
|
||||||
references = mail.references,
|
references=mail.references,
|
||||||
object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
|
object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
|
||||||
subtype = 'html',
|
subtype='html',
|
||||||
subtype_alternative = 'plain')
|
subtype_alternative='plain',
|
||||||
|
headers=headers)
|
||||||
res = ir_mail_server.send_email(cr, uid, msg,
|
res = ir_mail_server.send_email(cr, uid, msg,
|
||||||
mail_server_id=mail.mail_server_id.id, context=context)
|
mail_server_id=mail.mail_server_id.id,
|
||||||
|
context=context)
|
||||||
if res:
|
if res:
|
||||||
mail.write({'state': 'sent', 'message_id': res})
|
mail.write({'state': 'sent', 'message_id': res})
|
||||||
mail_sent = True
|
mail_sent = True
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<sheet>
|
<sheet>
|
||||||
<label for="subject" class="oe_edit_only"/>
|
<label for="subject" class="oe_edit_only"/>
|
||||||
<h2><field name="subject"/></h2>
|
<h2><field name="subject"/></h2>
|
||||||
<div>
|
<div style="vertical-align: top;">
|
||||||
by <field name="author_id" class="oe_inline" string="User"/> on <field name="date" class="oe_inline"/>
|
by <field name="author_id" class="oe_inline" string="User"/> on <field name="date" class="oe_inline"/>
|
||||||
<button name="%(action_email_compose_message_wizard)d" string="Reply" type="action" icon="terp-mail-replied"
|
<button name="%(action_email_compose_message_wizard)d" string="Reply" type="action" icon="terp-mail-replied"
|
||||||
context="{'default_composition_mode':'reply', 'default_parent_id': active_id}" states='received,sent,exception,cancel'/>
|
context="{'default_composition_mode':'reply', 'default_parent_id': active_id}" states='received,sent,exception,cancel'/>
|
||||||
|
@ -32,20 +32,30 @@
|
||||||
</page>
|
</page>
|
||||||
<page string="Advanced" groups="base.group_no_one">
|
<page string="Advanced" groups="base.group_no_one">
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<div>
|
||||||
<field name="auto_delete"/>
|
<group string="Status">
|
||||||
<field name="type"/>
|
<field name="auto_delete"/>
|
||||||
<field name="state"/>
|
<field name="type"/>
|
||||||
<field name="mail_server_id"/>
|
<field name="state"/>
|
||||||
<field name="model"/>
|
<field name="mail_server_id"/>
|
||||||
<field name="res_id"/>
|
<field name="model"/>
|
||||||
</group>
|
<field name="res_id"/>
|
||||||
<group>
|
</group>
|
||||||
<field name="message_id"/>
|
<group string="Tracking">
|
||||||
<field name="references"/>
|
<field name="opened"/>
|
||||||
<field name="partner_ids" widget="many2many_tags"/>
|
<field name="replied"/>
|
||||||
<field name="notified_partner_ids" widget="many2many_tags"/>
|
</group>
|
||||||
</group>
|
</div>
|
||||||
|
<div>
|
||||||
|
<group string="Headers">
|
||||||
|
<field name="message_id"/>
|
||||||
|
<field name="references"/>
|
||||||
|
</group>
|
||||||
|
<group string="Recipients">
|
||||||
|
<field name="partner_ids" widget="many2many_tags"/>
|
||||||
|
<field name="notified_partner_ids" widget="many2many_tags"/>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
<page string="Attachments">
|
<page string="Attachments">
|
||||||
|
|
|
@ -772,6 +772,7 @@ class mail_thread(osv.AbstractModel):
|
||||||
"""
|
"""
|
||||||
assert isinstance(message, Message), 'message must be an email.message.Message at this point'
|
assert isinstance(message, Message), 'message must be an email.message.Message at this point'
|
||||||
fallback_model = model
|
fallback_model = model
|
||||||
|
bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
|
||||||
|
|
||||||
# Get email.message.Message variables for future processing
|
# Get email.message.Message variables for future processing
|
||||||
message_id = message.get('Message-Id')
|
message_id = message.get('Message-Id')
|
||||||
|
@ -780,6 +781,24 @@ class mail_thread(osv.AbstractModel):
|
||||||
references = decode_header(message, 'References')
|
references = decode_header(message, 'References')
|
||||||
in_reply_to = decode_header(message, 'In-Reply-To')
|
in_reply_to = decode_header(message, 'In-Reply-To')
|
||||||
|
|
||||||
|
# 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
|
||||||
|
if bounce_alias in email_to:
|
||||||
|
bounce_match = tools.bounce_re.search(email_to)
|
||||||
|
if bounce_match:
|
||||||
|
bounced_mail_id = bounce_match.group(1)
|
||||||
|
if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id):
|
||||||
|
mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context)
|
||||||
|
bounced_model = mail.model
|
||||||
|
bounced_thread_id = mail.res_id
|
||||||
|
else:
|
||||||
|
bounced_model = bounce_match.group(2)
|
||||||
|
bounced_thread_id = int(bounce_match.group(3)) if bounce_match.group(3) else 0
|
||||||
|
_logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s',
|
||||||
|
email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id)
|
||||||
|
if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'):
|
||||||
|
self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context)
|
||||||
|
return []
|
||||||
|
|
||||||
# 1. Verify if this is a reply to an existing thread
|
# 1. Verify if this is a reply to an existing thread
|
||||||
thread_references = references or in_reply_to
|
thread_references = references or in_reply_to
|
||||||
ref_match = thread_references and tools.reference_re.search(thread_references)
|
ref_match = thread_references and tools.reference_re.search(thread_references)
|
||||||
|
@ -1018,6 +1037,15 @@ class mail_thread(osv.AbstractModel):
|
||||||
self.write(cr, uid, ids, update_vals, context=context)
|
self.write(cr, uid, ids, update_vals, context=context)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
|
||||||
|
"""Called by ``message_process`` when a bounce email (such as Undelivered
|
||||||
|
Mail Returned to Sender) is received for an existing thread. The default
|
||||||
|
behavior is to check is an integer ``message_bounce`` column exists.
|
||||||
|
If it is the case, its content is incremented. """
|
||||||
|
if self._all_columns.get('message_bounce'):
|
||||||
|
for obj in self.browse(cr, uid, ids, context=context):
|
||||||
|
self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
|
||||||
|
|
||||||
def _message_extract_payload(self, message, save_original=False):
|
def _message_extract_payload(self, message, save_original=False):
|
||||||
"""Extract body as HTML and attachments from the mail message"""
|
"""Extract body as HTML and attachments from the mail message"""
|
||||||
attachments = []
|
attachments = []
|
||||||
|
|
Loading…
Reference in New Issue