# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from freezegun import freeze_time from markupsafe import Markup from requests import Session, PreparedRequest, Response import datetime import werkzeug from odoo import tools from odoo.addons.mail.tests.common import mail_new_test_user from odoo.addons.mass_mailing.tests.common import MassMailCommon from odoo.tests import HttpCase, tagged from odoo.tools import mute_logger class TestMailingControllersCommon(MassMailCommon, HttpCase): @classmethod def setUpClass(cls): super(TestMailingControllersCommon, cls).setUpClass() # cleanup lists cls.env['mailing.list'].search([]).unlink() cls._create_mailing_list() cls.test_mailing_on_contacts = cls.env['mailing.mailing'].create({ 'body_html': '
Hello
Go to this link
Hello
Go to this link
Hello
Go to this link
Feedback from {test_email_normalized}
{test_feedback}
Blocklist request from portal of mailing {test_mailing.subject} (document Contact)
') ) self.assertEqual( msg_unbl.body, Markup(f'Blocklist removal request from portal of mailing {test_mailing.subject} (document Contact)
') ) self.assertEqual( msg_fb.body, Markup(f'Feedback from {test_email_normalized}
{test_feedback}
Blocklist request from unsubscribe link of mailing {test_mailing.subject} (document Contact)
') ) self.assertEqual(msg_create.body, Markup('Mail Blacklist created
')) def test_mailing_unsubscribe_from_document_tour_mailing_user(self): """ Test portal unsubscribe on mailings performed on documents (not mailing lists or contacts) using a generic '/unsubscribe' link allowing mailing users to see and edit unsubcribe page. Tour effects * unsubscribe from mailing based on a document = blocklist; * add feedback (block list): Other reason, with 'My feedback' feedback; * remove email from exclusion list; * re-add email to exclusion list; """ # update user to link it with existing mailing contacts and allow the tour # to run; test without and with mailing group self.user_marketing.write({ 'email': tools.formataddr(("Déboulonneur", "fleurus@example.com")), 'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)], }) test_mailing = self.test_mailing_on_documents.with_env(self.env) self.authenticate('user_marketing', 'user_marketing') # no group -> no direct access to /unsubscribe res = self.url_open( werkzeug.urls.url_join( test_mailing.get_base_url(), f'mailing/{test_mailing.id}/unsubscribe', ) ) self.assertEqual(res.status_code, 400) # group -> direct access to /unsubscribe should wokr self.user_marketing.write({ 'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)], }) # launch unsubscription tour with freeze_time(self._reference_now): self.start_tour( f"/mailing/{test_mailing.id}/unsubscribe", "mailing_portal_unsubscribe_from_document_with_lists", login=self.user_marketing.login, ) def test_mailing_unsubscribe_from_list_tour(self): """ Test portal unsubscribe on mailings performed on mailing lists. Their effect is to opt-out from the mailing list. Tour effects * unsubscribe from mailing based on lists = opt-out from lists; * add feedback (opt-out): Other reason, with 'My feedback' feedback; * add email to exclusion list; """ opt_out_reasons = self.env['mailing.subscription.optout'].search([]) test_mailing = self.test_mailing_on_lists.with_env(self.env) test_feedback = "My feedback" # fetch contact and its subscription and blacklist status, to see the tour effects contact_l1 = self.mailing_list_1.contact_ids.filtered( lambda contact: contact.email == self.test_email_normalized ) subscription_l1 = self.mailing_list_1.subscription_ids.filtered( lambda subscription: subscription.contact_id == contact_l1 ) # launch unsubscribe tour hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email) with freeze_time(self._reference_now): self.start_tour( f"/mailing/{test_mailing.id}/unsubscribe?email={self.test_email_normalized}&document_id={contact_l1.id}&hash_token={hash_token}", "mailing_portal_unsubscribe_from_list", login=None, ) # status update check on list 1 self.assertTrue(subscription_l1.opt_out) self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now) self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1]) # status update check on list 2: unmodified (was not member, still not member) contact_l2 = self.mailing_list_2.contact_ids.filtered( lambda contact: contact.email == self.test_email_normalized ) self.assertFalse(contact_l2) # posted messages on contact record for mailing list 1: feedback, unsubscription message_feedback = contact_l1.message_ids[0] self.assertEqual( message_feedback.body, Markup(f'Feedback from {contact_l1.email_normalized}
{test_feedback}
{contact_l1.display_name} unsubscribed from the following mailing list(s)
Blocklist request from portal of mailing {test_mailing.subject} (document Mailing Contact)
') ) self.assertEqual(msg_create.body, Markup('Mail Blacklist created
')) def test_mailing_unsubscribe_from_list_with_update_tour(self): """ Test portal unsubscribe on mailings performed on mailing lists. Their effect is to opt-out from the mailing list. Optional exclusion list can be done through interface (see tour). Tour effects * unsubscribe from mailing based on lists = opt-out from lists; * add feedback (opt-out): Other reason, with 'My feedback' feedback; * add email to exclusion list; * remove email from exclusion list; * come back to List3; * join List2 (with no feedback, as no opt-out / block list was done); * re-add email to exclusion list; """ opt_out_reasons = self.env['mailing.subscription.optout'].search([]) test_mailing = self.test_mailing_on_lists.with_env(self.env) test_feedback = "My feedback" # fetch contact and its subscription and blacklist status, to see the tour effects contact_l1 = self.mailing_list_1.contact_ids.filtered( lambda contact: contact.email == self.test_email_normalized ) subscription_l1 = self.mailing_list_1.subscription_ids.filtered( lambda subscription: subscription.contact_id == contact_l1 ) contact_l3 = self.mailing_list_3.contact_ids.filtered( lambda contact: contact.email == self.test_email_normalized ) subscription_l3 = self.mailing_list_3.subscription_ids.filtered( lambda subscription: subscription.contact_id == contact_l3 ) # launch unsubscription tour hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email) with freeze_time(self._reference_now): self.start_tour( f"/mailing/{test_mailing.id}/unsubscribe?email={contact_l1.email}&document_id={contact_l1.id}&hash_token={hash_token}", "mailing_portal_unsubscribe_from_list_with_update", login=None, ) # status update check on list 1 self.assertTrue(subscription_l1.opt_out) self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now) self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1]) # status update check on list 3 (opt-in during test) self.assertFalse(subscription_l3.opt_out) self.assertFalse(subscription_l3.opt_out_datetime) # posted messages on contact record for mailing list 1: subscription update, feedback, unsubscription message_update = contact_l1.message_ids[0] self.assertEqual( message_update.body, Markup(f'{contact_l1.display_name} subscribed to the following mailing list(s)
' f'Feedback from {contact_l1.email_normalized}
{test_feedback}
{contact_l1.display_name} unsubscribed from the following mailing list(s)
' f'{contact_l3.display_name} subscribed to the following mailing list(s)
' f'Blocklist request from portal of mailing {test_mailing.subject} (document Mailing Contact)
') ) self.assertEqual( msg_unbl.body, Markup(f'Blocklist removal request from portal of mailing {test_mailing.subject} (document Mailing Contact)
') ) self.assertEqual( msg_bl.body, Markup(f'Blocklist request from portal of mailing {test_mailing.subject} (document Mailing Contact)
') ) self.assertEqual(msg_create.body, Markup('Mail Blacklist created
')) def test_mailing_unsubscribe_from_my(self): """ Test portal unsubscribe using the 'my' mailing-specific portal page. It allows to opt-in / opt-out from mailing lists as well as to manage blocklist (see tour). Tour effects * opt-in List3 from opt-out, opt-in List2, opt-out List1; * add feedback (as new opt-out): Other reason, with 'My feedback' feedback; * add email in block list; * add feedback (as block list addition): First reason (hence no feedback); """ test_feedback = "My feedback" portal_user = mail_new_test_user( self.env, email=tools.formataddr(("Déboulonneur", "fleurus@example.com")), groups='base.group_portal', login='user_portal_fleurus', name='Déboulonneur User', signature='--\nDéboulonneur', ) _test_email, test_email_normalized = portal_user.email, portal_user.email_normalized opt_out_reasons = self.env['mailing.subscription.optout'].search([]) # launch 'my' mailing' tour self.authenticate(portal_user.login, portal_user.login) with freeze_time(self._reference_now): self.start_tour( "/mailing/my", "mailing_portal_unsubscribe_from_my", login=portal_user.login, ) # fetch contact and its subscription and blacklist status, to see the tour effects contact_l1 = self.mailing_list_1.contact_ids.filtered( lambda contact: contact.email == test_email_normalized ) subscription_l1 = self.mailing_list_1.subscription_ids.filtered( lambda subscription: subscription.contact_id == contact_l1 ) contact_l2 = self.mailing_list_2.contact_ids.filtered( lambda contact: contact.email == test_email_normalized ) subscription_l2 = self.mailing_list_2.subscription_ids.filtered( lambda subscription: subscription.contact_id == contact_l2 ) contact_l3 = self.mailing_list_3.contact_ids.filtered( lambda contact: contact.email == test_email_normalized ) subscription_l3 = self.mailing_list_3.subscription_ids.filtered( lambda subscription: subscription.contact_id == contact_l3 ) self.assertEqual(contact_l2, contact_l3, 'When creating new membership, should link with first found existing contact') self.assertTrue(contact_l1.is_blacklisted) self.assertTrue(contact_l3.is_blacklisted) self.assertTrue(subscription_l1.opt_out) self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now, 'Subscription: opt-outed during test, datetime should have been set') self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1]) self.assertFalse(subscription_l2.opt_out) self.assertFalse(subscription_l2.opt_out_datetime) self.assertFalse(subscription_l2.opt_out_reason_id) self.assertFalse(subscription_l3.opt_out) self.assertFalse(subscription_l3.opt_out_datetime, 'Subscription: opt-in during test, datetime should have been reset') self.assertFalse(subscription_l3.opt_out_reason_id) # message on contact for list 1: opt-out L1, join L2 msg_fb, msg_sub, msg_uns = contact_l1.message_ids self.assertEqual( msg_fb.body, Markup(f'Feedback from {portal_user.name} ({test_email_normalized})
{test_feedback}
{contact_l1.name} subscribed to the following mailing list(s)
' f'{contact_l1.name} unsubscribed from the following mailing list(s)
' f'Feedback from {portal_user.name} ({test_email_normalized})
{test_feedback}
{contact_l3.name} subscribed to the following mailing list(s)
' f'Blocklist request from portal
')) @mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view') def test_mailing_view(self): """ Test preview of mailing. It requires either a token, either being mailing user. """ test_mailing = self.test_mailing_on_documents.with_env(self.env) shadow_mailing = test_mailing.copy() doc_id, email_normalized = self.user_marketing.partner_id.id, self.user_marketing.email_normalized hash_token = test_mailing._generate_mailing_recipient_token(doc_id, email_normalized) self.user_marketing.write({ 'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)], }) self.authenticate('user_marketing', 'user_marketing') # TEST: various invalid cases for test_mid, test_doc_id, test_email, test_token, error_code in [ (test_mailing.id, doc_id, email_normalized, '', 400), # no token (test_mailing.id, doc_id, email_normalized, 'zboobs', 418), # wrong token (test_mailing.id, self.env.user.partner_id.id, email_normalized, hash_token, 418), # mismatch (test_mailing.id, doc_id, 'not.email@example.com', hash_token, 418), # mismatch (shadow_mailing.id, doc_id, email_normalized, hash_token, 418), # valid credentials but wrong mailing_id (0, doc_id, email_normalized, hash_token, 400), # valid credentials but missing mailing_id ]: with self.subTest(test_mid=test_mid, test_email=test_email, test_doc_id=test_doc_id, test_token=test_token): res = self.url_open( werkzeug.urls.url_join( test_mailing.get_base_url(), f'mailing/{test_mid}/view?email={test_email}&document_id={test_doc_id}&hash_token={test_token}', ) ) self.assertEqual(res.status_code, error_code) # TEST: valid call using credentials res = self.url_open( werkzeug.urls.url_join( test_mailing.get_base_url(), f'mailing/{test_mailing.id}/view?email={email_normalized}&document_id={doc_id}&hash_token={hash_token}', ) ) self.assertEqual(res.status_code, 200) # TEST: invalid credentials but mailing user self.user_marketing.write({ 'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)], }) res = self.url_open( werkzeug.urls.url_join( test_mailing.get_base_url(), f'mailing/{test_mailing.id}/view', ) ) self.assertEqual(res.status_code, 200) @tagged('link_tracker', 'mailing_portal') class TestMailingTracking(TestMailingControllersCommon): @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing') def test_tracking_short_code(self): """ Test opening short code linked to a mailing trace: should set the trace as opened and clicked, create a click record. """ mailing = self.test_mailing_on_lists.with_env(self.env) with self.mock_mail_gateway(mail_unlink_sent=False): mailing.action_send_mail() mail = self._find_mail_mail_wrecord(self.test_contact) mailing_trace = mail.mailing_trace_ids link_tracker_code = self._get_code_from_short_url( self._get_href_from_anchor_id(mail.body, 'url') ) self.assertEqual(len(link_tracker_code), 1) self.assertEqual(link_tracker_code.link_id.count, 0) self.assertEqual(mail.state, 'sent') self.assertEqual(len(mailing_trace), 1) self.assertFalse(mailing_trace.links_click_datetime) self.assertFalse(mailing_trace.open_datetime) self.assertEqual(mailing_trace.trace_status, 'sent') short_link_url = werkzeug.urls.url_join( mail.get_base_url(), f'r/{link_tracker_code.code}/m/{mailing_trace.id}' ) with freeze_time(self._reference_now): _response = self.url_open(short_link_url) self.assertEqual(link_tracker_code.link_id.count, 1) self.assertEqual(mailing_trace.links_click_datetime, self._reference_now) self.assertEqual(mailing_trace.open_datetime, self._reference_now) self.assertEqual(mailing_trace.trace_status, 'open') @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing') def test_tracking_url_token(self): """ Test tracking of mails linked to a mailing trace: should set the trace as opened. """ mailing = self.test_mailing_on_lists.with_env(self.env) with self.mock_mail_gateway(mail_unlink_sent=False): mailing.action_send_mail() mail = self._find_mail_mail_wrecord(self.test_contact) mail_id_int = mail.id mail_tracking_url = mail._get_tracking_url() mailing_trace = mail.mailing_trace_ids self.assertEqual(mail.state, 'sent') self.assertEqual(len(mailing_trace), 1) self.assertFalse(mailing_trace.open_datetime) self.assertEqual(mailing_trace.trace_status, 'sent') mail.unlink() # the mail might be removed during the email sending self.env.flush_all() with freeze_time(self._reference_now): response = self.url_open(mail_tracking_url) self.assertEqual(response.status_code, 200) self.assertEqual(mailing_trace.open_datetime, self._reference_now) self.assertEqual(mailing_trace.trace_status, 'open') track_url = werkzeug.urls.url_join( mailing.get_base_url(), f'mail/track/{mail_id_int}/fake_token/blank.gif' ) response = self.url_open(track_url) self.assertEqual(response.status_code, 401)