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()