From 1c7c837a10c25fe1a7377df0d6268b485e52482c Mon Sep 17 00:00:00 2001 From: std-odoo Date: Tue, 7 Apr 2020 11:46:12 +0000 Subject: [PATCH] [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) --- addons/mass_mailing/controllers/main.py | 8 +++++-- addons/mass_mailing/models/mail_mail.py | 11 +++++---- addons/mass_mailing/tests/__init__.py | 1 + .../tests/test_mailing_controllers.py | 22 ++++++++++++++++++ odoo/tools/misc.py | 23 +++++++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 addons/mass_mailing/tests/test_mailing_controllers.py diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 02ffe4609c3..b5ef9b9faca 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -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//blank.gif', type='http', auth='public') - def track_mail_open(self, mail_id, **post): + @http.route('/mail/track///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' diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index b92b6f1e478..4c627234c27 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -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 '' % 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, + '' % tracking_url, + plaintext=False, + ) body = self.env['mail.render.mixin']._replace_local_links(body) diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py index 49ff0a87784..ddc9e161eec 100644 --- a/addons/mass_mailing/tests/__init__.py +++ b/addons/mass_mailing/tests/__init__.py @@ -4,3 +4,4 @@ from . import common from . import test_mailing_internals from . import test_mailing_list +from . import test_mailing_controllers diff --git a/addons/mass_mailing/tests/test_mailing_controllers.py b/addons/mass_mailing/tests/test_mailing_controllers.py new file mode 100644 index 00000000000..eb1ecd60178 --- /dev/null +++ b/addons/mass_mailing/tests/test_mailing_controllers.py @@ -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) diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index ed81f6ccf86..24a58ba1922 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -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()