[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:
Thibault Delavallée 2013-08-06 17:11:43 +02:00
parent ade561e1cc
commit 38a534dee0
7 changed files with 136 additions and 45 deletions

View File

@ -273,6 +273,8 @@ class crm_lead(format_address, osv.osv):
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\'.'),
# Messaging and marketing
'message_bounce': fields.integer('Bounce'),
# Only used for type opportunity
'probability': fields.float('Success Rate (%)',group_operator="avg"),
'planned_revenue': fields.float('Expected Revenue', track_visibility='always'),

View File

@ -182,6 +182,7 @@
</group>
<group string="Mailings">
<field name="opt_out"/>
<field name="message_bounce"/>
</group>
<group string="Misc">
<field name="active"/>

View File

@ -1,17 +1,17 @@
import base64
import psycopg2
import openerp
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.http import request
#----------------------------------------------------------
# Controller
#----------------------------------------------------------
class MailController(oeweb.Controller):
class MailController(http.Controller):
_cp_path = '/mail'
@oeweb.httprequest
@http.httprequest
def download_attachment(self, req, model, id, method, attachment_id, **kw):
Model = req.session.model(model)
res = getattr(Model, method)(int(id), int(attachment_id))
@ -24,7 +24,7 @@ class MailController(oeweb.Controller):
('Content-Disposition', content_disposition(filename, req))])
return req.not_found()
@oeweb.jsonrequest
@http.jsonrequest
def receive(self, req):
""" End-point to receive mail from an external SMTP server. """
dbs = req.jsonrequest.get('databases')
@ -38,3 +38,10 @@ class MailController(oeweb.Controller):
except psycopg2.Error:
pass
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

View File

@ -46,6 +46,12 @@
<field name="value">catchall</field>
</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 -->
<record id="mt_comment" model="mail.message.subtype">
<field name="name">Discussions</field>

View File

@ -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
# and during unlink() we will not cascade delete the parent and its attachments
'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 = {
'state': 'outgoing',
}
@ -158,6 +159,18 @@ class mail_mail(osv.Model):
def cancel(self, cr, uid, ids, context=None):
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):
"""Send immediately queued messages, committing after each
message is sent - this is not transactional and should
@ -226,13 +239,22 @@ class mail_mail(osv.Model):
}
if mail.notification:
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)))
return _("""<small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small>""") % url
else:
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):
""" If subject is void and record_name defined: '<Author> posted on <Resource>'
@ -257,8 +279,11 @@ class mail_mail(osv.Model):
# generate footer
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:
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
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))
for partner in mail.recipient_ids:
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
res = None
for email in email_list:
msg = ir_mail_server.build_email(
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')
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)
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:
mail.write({'state': 'sent', 'message_id': res})
mail_sent = True

View File

@ -14,7 +14,7 @@
<sheet>
<label for="subject" class="oe_edit_only"/>
<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"/>
<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'/>
@ -32,20 +32,30 @@
</page>
<page string="Advanced" groups="base.group_no_one">
<group>
<group>
<field name="auto_delete"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
<group>
<field name="message_id"/>
<field name="references"/>
<field name="partner_ids" widget="many2many_tags"/>
<field name="notified_partner_ids" widget="many2many_tags"/>
</group>
<div>
<group string="Status">
<field name="auto_delete"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
<group string="Tracking">
<field name="opened"/>
<field name="replied"/>
</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>
</page>
<page string="Attachments">

View File

@ -772,6 +772,7 @@ class mail_thread(osv.AbstractModel):
"""
assert isinstance(message, Message), 'message must be an email.message.Message at this point'
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
message_id = message.get('Message-Id')
@ -780,6 +781,24 @@ class mail_thread(osv.AbstractModel):
references = decode_header(message, 'References')
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
thread_references = references or in_reply_to
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)
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):
"""Extract body as HTML and attachments from the mail message"""
attachments = []