[FIX] mass_mailing: improve opened traces tracking

Purpose of this commit is to improve tracking of opened traces. Notably
a token is added to ensure we do not mess with traces and have unique
tracking URLs.

MIGRATION REMARK

Emails sent before the migration will not be marked as opened anymore after
migration. We recommend to avoid sending statistically important mass mailings
about one week before migrating database. Indeed statistics show that most of
open emails happen within the first week after being sent.

Task 2223146

closes odoo/odoo#49139

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
std-odoo
2020-05-18 08:30:59 +00:00
committed by Thibault Delavallée
parent 59265847aa
commit 1c7c837a10
5 changed files with 59 additions and 6 deletions
+6 -2
View File
@@ -7,6 +7,7 @@ import werkzeug
from odoo import _, exceptions, http, tools
from odoo.http import request
from odoo.tools import consteq
from werkzeug.exceptions import BadRequest
class MassMailController(http.Controller):
@@ -91,9 +92,12 @@ class MassMailController(http.Controller):
return True
return 'error'
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='public')
def track_mail_open(self, mail_id, **post):
@http.route('/mail/track/<int:mail_id>/<string:token>/blank.gif', type='http', auth='public')
def track_mail_open(self, mail_id, token, **post):
""" Email tracking. """
if not consteq(token, tools.hmac(request.env(su=True), 'mass_mailing-mail_mail-open', mail_id)):
raise BadRequest()
request.env['mailing.trace'].sudo().set_opened(mail_mail_ids=[mail_id])
response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif'
+7 -4
View File
@@ -26,8 +26,8 @@ class MailMail(models.Model):
def _get_tracking_url(self):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
track_url = werkzeug.urls.url_join(base_url, 'mail/track/%s/blank.gif' % self.id)
return '<img src="%s" alt=""/>' % track_url
token = tools.hmac(self.env(su=True), 'mass_mailing-mail_mail-open', self.id)
return werkzeug.urls.url_join(base_url, 'mail/track/%s/%s/blank.gif' % (self.id, token))
def _get_unsubscribe_url(self, email_to):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
@@ -64,8 +64,11 @@ class MailMail(models.Model):
# generate tracking URL
tracking_url = self._get_tracking_url()
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
body = tools.append_content_to_html(
body,
'<img src="%s"/>' % tracking_url,
plaintext=False,
)
body = self.env['mail.render.mixin']._replace_local_links(body)
+1
View File
@@ -4,3 +4,4 @@
from . import common
from . import test_mailing_internals
from . import test_mailing_list
from . import test_mailing_controllers
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo.addons.mass_mailing.tests.common import TestMassMailCommon
from odoo.tests.common import HttpCase
class TestMassMailingControllers(TestMassMailCommon, HttpCase):
def test_tracking_url_token(self):
mail_mail = self.env['mail.mail'].create({})
response = self.url_open(mail_mail._get_tracking_url())
self.assertEqual(response.status_code, 200)
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
url = werkzeug.urls.url_join(base_url, 'mail/track/%s/fake_token/blank.gif' % mail_mail.id)
response = self.url_open(url)
self.assertEqual(response.status_code, 400)
+23
View File
@@ -8,6 +8,8 @@ Miscellaneous tools used by OpenERP.
import cProfile
import collections
import datetime
import hmac as hmac_lib
import hashlib
import io
import os
import pickle as pickle_
@@ -1530,3 +1532,24 @@ def traverse_containers(val, type_):
elif isinstance(val, collections.abc.Sequence):
for v in val:
yield from traverse_containers(v, type_)
def hmac(env, scope, message, hash_function=hashlib.sha256):
"""Compute HMAC with `database.secret` config parameter as key.
:param env: sudo environment to use for retrieving config parameter
:param message: message to authenticate
:param scope: scope of the authentication, to have different signature for the same
message in different usage
:param hash_function: hash function to use for HMAC (default: SHA-256)
"""
if not scope:
raise ValueError('Non-empty scope required')
secret = env['ir.config_parameter'].get_param('database.secret')
message = repr((scope, message))
return hmac_lib.new(
secret.encode(),
message.encode(),
hash_function,
).hexdigest()