[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,
|
||||
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'),
|
||||
|
|
|
@ -182,6 +182,7 @@
|
|||
</group>
|
||||
<group string="Mailings">
|
||||
<field name="opt_out"/>
|
||||
<field name="message_bounce"/>
|
||||
</group>
|
||||
<group string="Misc">
|
||||
<field name="active"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue