Initial commit: Mail packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 4e53507711
1948 changed files with 751201 additions and 0 deletions

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_blacklist
from . import test_blacklist_behavior
from . import test_blacklist_mixin
from . import test_link_tracker
from . import test_link_tracker_sms
from . import test_mailing
from . import test_mailing_server
from . import test_mailing_sms
from . import test_mailing_statistics
from . import test_mailing_statistics_sms
from . import test_mailing_test
from . import test_performance
from . import test_utm

View file

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.mass_mailing_sms.tests.common import MassSMSCommon
from odoo.addons.test_mail_sms.tests.common import TestSMSCommon
class TestMassMailCommon(MassSMSCommon, TestSMSCommon):
@classmethod
def setUpClass(cls):
super(TestMassMailCommon, cls).setUpClass()
cls.test_alias = cls.env['mail.alias'].create({
'alias_name': 'test.alias',
'alias_user_id': False,
'alias_model_id': cls.env['ir.model']._get('mailing.test.simple').id,
'alias_contact': 'everyone'
})
# enforce last update by user_marketing to match _process_mass_mailing_queue
# taking last writer as user running a batch
cls.mailing_bl = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
'name': 'SourceName',
'subject': 'MailingSubject',
# `+ ""` is for insuring that _prepend_preview rule out that case
'preview': 'Hi {{ object.name + "" }} :)',
'body_html': """<div><p>Hello <t t-out="object.name"/></p>,
<t t-set="url" t-value="'www.odoo.com'"/>
<t t-set="httpurl" t-value="'https://www.odoo.eu'"/>f
<span>Website0: <a id="url0" t-attf-href="https://www.odoo.tz/my/{{object.name}}">https://www.odoo.tz/my/<t t-out="object.name"/></a></span>
<span>Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</a></span>
<span>Website2: <a id="url2" t-attf-href="https://{{url}}">https://<t t-out="url"/></a></span>
<span>Website3: <a id="url3" t-att-href="httpurl"><t t-out="httpurl"/></a></span>
<span>External1: <a id="url4" href="https://www.example.com/foo/bar?baz=qux">Youpie</a></span>
<span>Internal1: <a id="url5" href="/event/dummy-event-0">Internal link</a></span>
<span>Internal2: <a id="url6" href="/view"/>View link</a></span>
<span>Email: <a id="url7" href="mailto:test@odoo.com">test@odoo.com</a></span>
<p>Stop spam ? <a id="url8" role="button" href="/unsubscribe_from_list">Ok</a></p>
</div>""",
'mailing_type': 'mail',
'mailing_model_id': cls.env['ir.model']._get('mailing.test.blacklist').id,
'reply_to_mode': 'update',
})
cls.mailing_sms = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
'name': 'XMas SMS',
'subject': 'Xmas SMS for {object.name}',
'mailing_model_id': cls.env['ir.model']._get('mail.test.sms').id,
'mailing_type': 'sms',
'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
'body_plaintext': 'Dear {{object.display_name}} this is a mass SMS with two links http://www.odoo.com/smstest and http://www.odoo.com/smstest/{{object.id}}',
'sms_force_send': True,
'sms_allow_unsubscribe': True,
})
@classmethod
def _create_test_blacklist_records(cls, model='mailing.test.blacklist', count=1):
""" Deprecated, remove in 14.4 """
return cls.__create_mailing_test_records(model=model, count=count)
@classmethod
def _create_mailing_sms_test_records(cls, model='mail.test.sms', partners=None, count=1):
""" Helper to create data. Currently simple, to be improved. """
Model = cls.env[model]
phone_field = 'phone_nbr' if 'phone_nbr' in Model else 'phone'
partner_field = 'customer_id' if 'customer_id' in Model else 'partner_id'
vals_list = []
for idx in range(count):
vals = {
'name': 'MassSMSTestRecord_%02d' % idx,
phone_field: '045600%02d%02d' % (idx, idx)
}
if partners:
vals[partner_field] = partners[idx % len(partners)]
vals_list.append(vals)
return cls.env[model].create(vals_list)
@classmethod
def _create_mailing_test_records(cls, model='mailing.test.blacklist', partners=None, count=1):
""" Helper to create data. Currently simple, to be improved. """
Model = cls.env[model]
email_field = 'email' if 'email' in Model else 'email_from'
partner_field = 'customer_id' if 'customer_id' in Model else 'partner_id'
vals_list = []
for x in range(0, count):
vals = {
'name': 'TestRecord_%02d' % x,
email_field: '"TestCustomer %02d" <test.record.%02d@test.example.com>' % (x, x),
}
if partners:
vals[partner_field] = partners[x % len(partners)]
vals_list.append(vals)
return cls.env[model].create(vals_list)
class TestMassSMSCommon(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMassSMSCommon, cls).setUpClass()
cls._test_body = 'Mass SMS in your face'
records = cls.env['mail.test.sms']
partners = cls.env['res.partner']
country_be_id = cls.env.ref('base.be').id
_country_us_id = cls.env.ref('base.us').id
for x in range(10):
partners += cls.env['res.partner'].with_context(**cls._test_context).create({
'name': 'Partner_%s' % (x),
'email': '_test_partner_%s@example.com' % (x),
'country_id': country_be_id,
'mobile': '045600%s%s99' % (x, x)
})
records += cls.env['mail.test.sms'].with_context(**cls._test_context).create({
'name': 'MassSMSTest_%s' % (x),
'customer_id': partners[x].id,
'phone_nbr': '045600%s%s44' % (x, x)
})
cls.records = cls._reset_mail_context(records)
cls.records_numbers = [phone_validation.phone_format(r.phone_nbr, 'BE', '32', force_format='E164') for r in cls.records]
cls.partners = partners
cls.sms_template = cls.env['sms.template'].create({
'name': 'Test Template',
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
'body': 'Dear {{ object.display_name }} this is a mass SMS.',
})
cls.partner_numbers = [
phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
for partner in partners
]

View file

@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import users
from odoo.addons.test_mass_mailing.tests import common
from odoo.exceptions import AccessError
class TestBLAccessRights(common.TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestBLAccessRights, cls).setUpClass()
cls._create_portal_user()
cls.bl_rec = cls.env['mail.blacklist'].create([
{'email': 'Not A Stark <john.snow@example.com>'},
])
cls.bl_previous = cls.env['mail.blacklist'].search([])
@users('employee')
def test_bl_crud_employee(self):
with self.assertRaises(AccessError):
self.env['mail.blacklist'].create([{'email': 'Arya.Stark@example.com'}])
with self.assertRaises(AccessError):
self.bl_rec.with_user(self.env.user).read([])
with self.assertRaises(AccessError):
self.bl_rec.with_user(self.env.user).write({'email': 'jaimie.lannister@example.com'})
with self.assertRaises(AccessError):
self.bl_rec.with_user(self.env.user).unlink()
@users('portal_test')
def test_bl_crud_portal(self):
with self.assertRaises(AccessError):
self.env['mail.blacklist'].create([{'email': 'Arya.Stark@example.com'}])
with self.assertRaises(AccessError):
self.bl_rec.with_user(self.env.user).read([])
with self.assertRaises(AccessError):
self.bl_rec.with_user(self.env.user).write({'email': 'jaimie.lannister@example.com'})
with self.assertRaises(AccessError):
self.bl_rec.with_user(self.env.user).unlink()
@users('user_marketing')
def test_bl_crud_marketing(self):
self.env['mail.blacklist'].create([{'email': 'Arya.Stark@example.com'}])
read_res = self.bl_rec.with_user(self.env.user).read([])
self.assertEqual(read_res[0]['id'], self.bl_rec.id)
self.bl_rec.with_user(self.env.user).write({'email': 'jaimie.lannister@example.com'})
self.assertEqual(self.bl_rec.email, 'jaimie.lannister@example.com')
self.bl_rec.with_user(self.env.user).unlink()
class TestBLConsistency(common.TestMassMailCommon):
_base_list = ['Arya.Stark@example.com', 'ned.stark@example.com']
def setUp(self):
super(TestBLConsistency, self).setUp()
self.bl_rec = self.env['mail.blacklist'].create([
{'email': 'Not A Stark <john.snow@example.com>'},
])
self.bl_previous = self.env['mail.blacklist'].search([])
@users('user_marketing')
def test_bl_check_case_add(self):
""" Test emails case when adding through _add """
bl_sudo = self.env['mail.blacklist'].sudo()
existing = bl_sudo.create({
'email': 'arya.stark@example.com',
'active': False,
})
added = self.env['mail.blacklist']._add('Arya.Stark@EXAMPLE.com')
self.assertEqual(existing, added)
self.assertTrue(existing.active)
@users('user_marketing')
def test_bl_check_case_remove(self):
""" Test emails case when deactivating through _remove """
bl_sudo = self.env['mail.blacklist'].sudo()
existing = bl_sudo.create({
'email': 'arya.stark@example.com',
'active': True,
})
added = self.env['mail.blacklist']._remove('Arya.Stark@EXAMPLE.com')
self.assertEqual(existing, added)
self.assertFalse(existing.active)
@users('user_marketing')
def test_bl_create_duplicate(self):
""" Test emails are inserted only once if duplicated """
bl_sudo = self.env['mail.blacklist'].sudo()
self.env['mail.blacklist'].create([
{'email': self._base_list[0]},
{'email': self._base_list[1]},
{'email': 'Another Ned Stark <%s>' % self._base_list[1]},
])
new_bl = bl_sudo.search([('id', 'not in', self.bl_previous.ids)])
self.assertEqual(len(new_bl), 2)
self.assertEqual(
set(v.lower() for v in self._base_list),
set(v.lower() for v in new_bl.mapped('email'))
)
@users('user_marketing')
def test_bl_create_parsing(self):
""" Test email is correctly extracted from given entries """
bl_sudo = self.env['mail.blacklist'].sudo()
self.env['mail.blacklist'].create([
{'email': self._base_list[0]},
{'email': self._base_list[1]},
{'email': 'Not Ned Stark <jaimie.lannister@example.com>'},
])
new_bl = bl_sudo.search([('id', 'not in', self.bl_previous.ids)])
self.assertEqual(len(new_bl), 3)
self.assertEqual(
set(v.lower() for v in self._base_list + ['jaimie.lannister@example.com']),
set(v.lower() for v in new_bl.mapped('email'))
)
@users('user_marketing')
def test_bl_search_exact(self):
search_res = self.env['mail.blacklist'].search([('email', '=', 'john.snow@example.com')])
self.assertEqual(search_res, self.bl_rec)
@users('user_marketing')
def test_bl_search_parsing(self):
search_res = self.env['mail.blacklist'].search([('email', '=', 'Not A Stark <john.snow@example.com>')])
self.assertEqual(search_res, self.bl_rec)
search_res = self.env['mail.blacklist'].search([('email', '=', '"John J. Snow" <john.snow@example.com>')])
self.assertEqual(search_res, self.bl_rec)
search_res = self.env['mail.blacklist'].search([('email', '=', 'Aegon? <john.snow@example.com>')])
self.assertEqual(search_res, self.bl_rec)
search_res = self.env['mail.blacklist'].search([('email', '=', '"John; \"You know Nothing\" Snow" <john.snow@example.com>')])
self.assertEqual(search_res, self.bl_rec)
@users('user_marketing')
def test_bl_search_case(self):
search_res = self.env['mail.blacklist'].search([('email', '=', 'john.SNOW@example.COM>')])
self.assertEqual(search_res, self.bl_rec)
@users('user_marketing')
def test_bl_search_partial(self):
search_res = self.env['mail.blacklist'].search([('email', 'ilike', 'John')])
self.assertEqual(search_res, self.bl_rec)
search_res = self.env['mail.blacklist'].search([('email', 'ilike', 'n.SNOW@example.cO>')])
self.assertEqual(search_res, self.bl_rec)

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from freezegun import freeze_time
from unittest.mock import patch
from odoo.addons.mass_mailing.models.mail_thread import BLACKLIST_MAX_BOUNCED_LIMIT
from odoo.addons.test_mass_mailing.tests import common
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
from odoo.sql_db import Cursor
@tagged('mail_blacklist')
class TestAutoBlacklist(common.TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestAutoBlacklist, cls).setUpClass()
cls.target_rec = cls._create_mailing_test_records()[0]
cls.mailing_bl.write({'mailing_domain': [('id', 'in', cls.target_rec.ids)]})
@users('user_marketing')
def test_mailing_bounce_w_auto_bl(self):
self._test_mailing_bounce_w_auto_bl(None)
@users('user_marketing')
def test_mailing_bounce_w_auto_bl_partner(self):
bounced_partner = self.env['res.partner'].sudo().create({
'name': 'Bounced Partner',
'email': self.target_rec.email_from,
'message_bounce': BLACKLIST_MAX_BOUNCED_LIMIT,
})
self._test_mailing_bounce_w_auto_bl({'bounced_partner': bounced_partner})
@users('user_marketing')
def test_mailing_bounce_w_auto_bl_partner_duplicates(self):
bounced_partners = self.env['res.partner'].sudo().create({
'name': 'Bounced Partner1',
'email': self.target_rec.email_from,
'message_bounce': BLACKLIST_MAX_BOUNCED_LIMIT,
}) | self.env['res.partner'].sudo().create({
'name': 'Bounced Partner2',
'email': self.target_rec.email_from,
'message_bounce': BLACKLIST_MAX_BOUNCED_LIMIT,
})
self._test_mailing_bounce_w_auto_bl({'bounced_partner': bounced_partners})
@mute_logger('odoo.addons.mail.models.mail_thread')
def _test_mailing_bounce_w_auto_bl(self, bounce_base_values):
mailing = self.mailing_bl.with_env(self.env)
target = self.target_rec.with_env(self.env)
# create bounced history of 4 statistics
traces = self.env['mailing.trace']
for idx in range(4):
new_mailing = mailing.copy()
new_dt = datetime.datetime.now() - datetime.timedelta(weeks=idx+2)
# Cursor.now() uses transaction's timestamp and not datetime lib -> freeze_time
# is not sufficient
with freeze_time(new_dt), patch.object(Cursor, 'now', lambda *args, **kwargs: new_dt):
traces += self._create_bounce_trace(new_mailing, target, dt=datetime.datetime.now() - datetime.timedelta(weeks=idx+2))
self.gateway_mail_bounce(new_mailing, target, bounce_base_values)
# mass mail record: ok, not blacklisted yet
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com'}],
mailing, target,
check_mail=True
)
# call bounced
self.gateway_mail_bounce(mailing, target, bounce_base_values)
# check blacklist
blacklist_record = self.env['mail.blacklist'].sudo().search([('email', '=', target.email_normalized)])
self.assertEqual(len(blacklist_record), 1)
self.assertTrue(target.is_blacklisted)
# mass mail record: ko, blacklisted
new_mailing = mailing.copy({'mailing_domain': [('id', 'in', target.ids)]})
with self.mock_mail_gateway(mail_unlink_sent=False):
new_mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
new_mailing, target, check_mail=True
)

View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.models.mailing_models import MailingBLacklist
from odoo.addons.test_mass_mailing.tests import common
from odoo.exceptions import UserError
from odoo.tests.common import users
class TestBLMixin(common.TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestBLMixin, cls).setUpClass()
cls.env['mail.blacklist'].create([{
'email': 'Arya.Stark@example.com',
'active': True,
}, {
'email': 'Sansa.Stark@example.com',
'active': False,
}])
@users('employee')
def test_bl_mixin_primary_field_consistency(self):
MailingBLacklist._primary_email = 'not_a_field'
with self.assertRaises(UserError):
self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
MailingBLacklist._primary_email = ['not_a_str']
with self.assertRaises(UserError):
self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
MailingBLacklist._primary_email = 'email_from'
self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
@users('employee')
def test_bl_mixin_is_blacklisted(self):
""" Test is_blacklisted field computation """
record = self.env['mailing.test.blacklist'].create({'email_from': 'arya.stark@example.com'})
self.assertTrue(record.is_blacklisted)
record = self.env['mailing.test.blacklist'].create({'email_from': 'not.arya.stark@example.com'})
self.assertFalse(record.is_blacklisted)
@users('employee')
def test_bl_mixin_search_blacklisted(self):
""" Test is_blacklisted field search implementation """
record1 = self.env['mailing.test.blacklist'].create({'email_from': 'arya.stark@example.com'})
record2 = self.env['mailing.test.blacklist'].create({'email_from': 'not.arya.stark@example.com'})
search_res = self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
self.assertEqual(search_res, record2)
search_res = self.env['mailing.test.blacklist'].search([('is_blacklisted', '!=', True)])
self.assertEqual(search_res, record2)
search_res = self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', True)])
self.assertEqual(search_res, record1)
search_res = self.env['mailing.test.blacklist'].search([('is_blacklisted', '!=', False)])
self.assertEqual(search_res, record1)
@users('employee')
def test_bl_mixin_search_blacklisted_format(self):
""" Test is_blacklisted field search using email parsing """
record1 = self.env['mailing.test.blacklist'].create({'email_from': 'Arya Stark <arya.stark@example.com>'})
self.assertTrue(record1.is_blacklisted)
search_res = self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', True)])
self.assertEqual(search_res, record1)

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import users
from odoo.addons.test_mass_mailing.tests import common
class TestLinkTracker(common.TestMassMailCommon):
def setUp(self):
super(TestLinkTracker, self).setUp()
self.link = self.env['link.tracker'].search_or_create({
'url': 'https://www.example.com'
})
self.click = self.env['link.tracker.click'].create({
'link_id': self.link.id,
'ip': '100.00.00.00',
'country_id': self.env.ref('base.fr').id,
})
def test_add_link(self):
code = self.link.code
self.assertEqual(self.link.count, 1)
# click from a new IP should create a new entry
click = self.env['link.tracker.click'].sudo().add_click(
code,
ip='100.00.00.01',
country_code='BEL'
)
self.assertEqual(click.ip, '100.00.00.01')
self.assertEqual(click.country_id, self.env.ref('base.be'))
self.assertEqual(self.link.count, 2)
# click from same IP (even another country) does not create a new entry
click = self.env['link.tracker.click'].sudo().add_click(
code,
ip='100.00.00.01',
country_code='FRA'
)
self.assertEqual(click, None)
self.assertEqual(self.link.count, 2)
@users('user_marketing')
def test_add_link_mail_stat(self):
record = self.env['mailing.test.blacklist'].create({})
code = self.link.code
self.assertEqual(self.link.count, 1)
trace = self.env['mailing.trace'].create({
'mass_mailing_id': self.mailing_bl.id,
'model': record._name,
'res_id': record.id,
})
self.assertEqual(trace.trace_status, 'outgoing')
self.assertFalse(trace.links_click_datetime)
# click from a new IP should create a new entry and update stat when provided
click = self.env['link.tracker.click'].sudo().add_click(
code,
ip='100.00.00.01',
country_code='BEL',
mailing_trace_id=trace.id
)
self.assertEqual(self.link.count, 2)
self.assertEqual(click.mass_mailing_id, self.mailing_bl)
self.assertTrue(trace.trace_status, 'open')
self.assertTrue(trace.links_click_datetime)
self.assertEqual(trace.links_click_ids, click)

View file

@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.tests import tagged
@tagged('link_tracker')
class TestSMSPost(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestSMSPost, cls).setUpClass()
cls._test_body = 'VOID CONTENT'
cls.sms_all = cls.env['sms.sms']
for x in range(10):
cls.sms_all |= cls.env['sms.sms'].create({
'number': '+324560000%s%s' % (x, x),
'body': cls._test_body,
})
# tracking info
cls.utm_c = cls.env['utm.campaign'].create({
'name': 'UTM C',
'stage_id': cls.env.ref('utm.default_utm_stage').id,
'is_auto_campaign': True,
})
cls.utm_m = cls.env.ref('mass_mailing_sms.utm_medium_sms')
cls.tracker_values = {
'campaign_id': cls.utm_c.id,
'medium_id': cls.utm_m.id,
}
def setUp(self):
super(TestSMSPost, self).setUp()
self._web_base_url = 'https://test.odoo.com'
self.env['ir.config_parameter'].sudo().set_param('web.base.url', self._web_base_url)
def test_body_link_shorten(self):
link = 'http://www.example.com'
self.env['link.tracker'].search([('url', '=', link)]).unlink()
new_body = self.env['mail.render.mixin']._shorten_links_text('Welcome to %s !' % link, self.tracker_values)
self.assertNotIn(link, new_body)
self.assertLinkShortenedText(new_body, (link, True), {'utm_campaign': self.utm_c.name, 'utm_medium': self.utm_m.name})
link = self.env['link.tracker'].search([('url', '=', link)])
self.assertIn(link.short_url, new_body)
link = f'{self._web_base_url}/my/super_page?test[0]=42&toto=áâà#title3'
self.env['link.tracker'].search([('url', '=', link)]).unlink()
new_body = self.env['mail.render.mixin']._shorten_links_text('Welcome to %s !' % link, self.tracker_values)
self.assertNotIn(link, new_body)
self.assertLinkShortenedText(new_body, (link, True), {
'utm_campaign': self.utm_c.name,
'utm_medium': self.utm_m.name,
'test[0]': '42',
'toto': 'áâà',
})
link = self.env['link.tracker'].search([('url', '=', link)])
self.assertIn(link.short_url, new_body)
# Bugfix: ensure void content convert does not crash
new_body = self.env['mail.render.mixin']._shorten_links_text(False, self.tracker_values)
self.assertFalse(new_body)
def test_body_link_shorten_wshort(self):
link = f'{self._web_base_url}/r/RAOUL'
self.env['link.tracker'].search([('url', '=', link)]).unlink()
new_body = self.env['mail.render.mixin']._shorten_links_text('Welcome to %s !' % link, self.tracker_values)
self.assertIn(link, new_body)
self.assertFalse(self.env['link.tracker'].search([('url', '=', link)]))
def test_body_link_shorten_wunsubscribe(self):
link = f'{self._web_base_url}/sms/3/'
self.env['link.tracker'].search([('url', '=', link)]).unlink()
new_body = self.env['mail.render.mixin']._shorten_links_text('Welcome to %s !' % link, self.tracker_values)
self.assertIn(link, new_body)
self.assertFalse(self.env['link.tracker'].search([('url', '=', link)]))
def test_sms_body_link_shorten_suffix(self):
mailing = self.env['mailing.mailing'].create({
'subject': 'Minimal mailing',
'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
'mailing_type': 'sms',
})
sms_0 = self.env['sms.sms'].create({
'body': f'Welcome to {self._web_base_url}',
'number': '10',
'mailing_id': mailing.id,
})
sms_1 = self.env['sms.sms'].create({
'body': f'Welcome to {self._web_base_url}/r/RAOUL',
'number': '11',
})
sms_2 = self.env['sms.sms'].create({
'body': f'Welcome to {self._web_base_url}/r/RAOUL',
'number': '12',
'mailing_id': mailing.id,
})
sms_3 = self.env['sms.sms'].create({
'body': f'Welcome to {self._web_base_url}/leodagan/r/RAOUL',
'number': '13',
'mailing_id': mailing.id,
})
sms_4 = self.env['sms.sms'].create({
'body': f'Welcome to {self._web_base_url}/r/RAOUL\nAnd again,\n'
f'{self._web_base_url}/r/RAOUL',
'number': '14',
'mailing_id': mailing.id,
})
res = (sms_0 | sms_1 | sms_2 | sms_3 | sms_4)._update_body_short_links()
self.assertEqual(res[sms_0.id], f'Welcome to {self._web_base_url}')
self.assertEqual(res[sms_1.id], f'Welcome to {self._web_base_url}/r/RAOUL')
self.assertEqual(res[sms_2.id], f'Welcome to {self._web_base_url}/r/RAOUL/s/%s' % sms_2.id)
self.assertEqual(res[sms_3.id], f'Welcome to {self._web_base_url}/leodagan/r/RAOUL')
self.assertEqual(
res[sms_4.id],
f'Welcome to {self._web_base_url}/r/RAOUL/s/{sms_4.id}\nAnd again,\n{self._web_base_url}/r/RAOUL/s/{sms_4.id}')

View file

@ -0,0 +1,590 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.data.mail_test_data import MAIL_TEMPLATE
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger, email_normalize
@tagged('mass_mailing')
class TestMassMailing(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMassMailing, cls).setUpClass()
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_mailing_gateway_reply(self):
customers = self.env['res.partner']
for x in range(0, 3):
customers |= self.env['res.partner'].create({
'name': 'Customer_%02d' % x,
'email': '"Customer_%02d" <customer_%02d@test.example.com' % (x, x),
})
mailing = self.env['mailing.mailing'].create({
'name': 'TestName',
'subject': 'TestSubject',
'body_html': 'Hello <t t-out="object.name" />',
'reply_to_mode': 'new',
'reply_to': '%s@%s' % (self.test_alias.alias_name, self.test_alias.alias_domain),
'keep_archives': True,
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'mailing_domain': '%s' % [('id', 'in', customers.ids)],
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[1], use_in_reply_to=False)
# customer2 looses headers
mail_mail = self._find_mail_mail_wrecord(customers[2])
self.format_and_process(
MAIL_TEMPLATE,
mail_mail.email_to,
mail_mail.reply_to,
subject='Re: %s' % mail_mail.subject,
extra='',
msg_id='<123456.%s.%d@test.example.com>' % (customers[2]._name, customers[2].id),
target_model=customers[2]._name, target_field=customers[2]._rec_name,
)
mailing.flush_recordset()
# check traces status
traces = self.env['mailing.trace'].search([('model', '=', customers._name), ('res_id', 'in', customers.ids)])
self.assertEqual(len(traces), 3)
customer0_trace = traces.filtered(lambda t: t.res_id == customers[0].id)
self.assertEqual(customer0_trace.trace_status, 'reply')
customer1_trace = traces.filtered(lambda t: t.res_id == customers[1].id)
self.assertEqual(customer1_trace.trace_status, 'reply')
customer2_trace = traces.filtered(lambda t: t.res_id == customers[2].id)
self.assertEqual(customer2_trace.trace_status, 'sent')
# check mailing statistics
self.assertEqual(mailing.sent, 3)
self.assertEqual(mailing.delivered, 3)
self.assertEqual(mailing.opened, 2)
self.assertEqual(mailing.replied, 2)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_gateway_update(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.optout', count=5)
self.assertEqual(len(recipients), 5)
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout'),
'mailing_domain': [('id', 'in', recipients.ids)]
})
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': record.email_normalized}
for record in recipients],
mailing, recipients,
mail_links_info=[[
('url0', 'https://www.odoo.tz/my/%s' % record.name, True, {}),
('url1', 'https://www.odoo.be', True, {}),
('url2', 'https://www.odoo.com', True, {}),
('url3', 'https://www.odoo.eu', True, {}),
('url4', 'https://www.example.com/foo/bar?baz=qux', True, {'baz': 'qux'}),
('url5', '%s/event/dummy-event-0' % mailing.get_base_url(), True, {}),
# view is not shortened and parsed at sending
('url6', '%s/view' % mailing.get_base_url(), False, {}),
('url7', 'mailto:test@odoo.com', False, {}),
# unsubscribe is not shortened and parsed at sending
('url8', '%s/unsubscribe_from_list' % mailing.get_base_url(), False, {}),
] for record in recipients],
check_mail=True
)
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5)
# simulate a click
self.gateway_mail_click(mailing, recipients[0], 'https://www.odoo.be')
mailing.invalidate_recordset()
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5, opened=1, clicked=1)
# simulate a bounce
self.assertEqual(recipients[1].message_bounce, 0)
self.gateway_mail_bounce(mailing, recipients[1])
mailing.invalidate_recordset()
self.assertMailingStatistics(mailing, expected=5, delivered=4, sent=5, opened=1, clicked=1, bounced=1)
self.assertEqual(recipients[1].message_bounce, 1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_recipients(self):
""" Test recipient-specific computation, with email, formatting,
multi-emails, ... to test corner cases. Blacklist mixin impact is
tested. """
(customer_mult, customer_fmt, customer_unic,
customer_case, customer_weird, customer_weird_2
) = self.env['res.partner'].create([
{
'email': 'customer.multi.1@example.com, "Test Multi 2" <customer.multi.2@example.com>',
'name': 'MultiEMail',
}, {
'email': '"Formatted Customer" <test.customer.format@example.com>',
'name': 'FormattedEmail',
}, {
'email': '"Unicode Customer" <test.customer.😊@example.com>',
'name': 'UnicodeEmail',
}, {
'email': 'TEST.CUSTOMER.CASE@EXAMPLE.COM',
'name': 'CaseEmail',
}, {
'email': 'test.customer.weird@example.com Weird Format',
'name': 'WeirdFormatEmail',
}, {
'email': 'Weird Format2 test.customer.weird.2@example.com',
'name': 'WeirdFormatEmail2',
}
])
# check difference of email management between a classic model and a model
# with an 'email_normalized' field (blacklist mixin)
for dst_model in ['mailing.test.customer', 'mailing.test.blacklist']:
with self.subTest(dst_model=dst_model):
(record_p_mult, record_p_fmt, record_p_unic,
record_p_case, record_p_weird, record_p_weird_2,
record_mult, record_fmt, record_unic,
record_case, recod_weird, record_weird_2
) = self.env[dst_model].create([
{
'customer_id': customer_mult.id,
}, {
'customer_id': customer_fmt.id,
}, {
'customer_id': customer_unic.id,
}, {
'customer_id': customer_case.id,
}, {
'customer_id': customer_weird.id,
}, {
'customer_id': customer_weird_2.id,
}, {
'email_from': 'record.multi.1@example.com, "Record Multi 2" <record.multi.2@example.com>',
}, {
'email_from': '"Formatted Record" <record.format@example.com>',
}, {
'email_from': '"Unicode Record" <record.😊@example.com>',
}, {
'email_from': 'TEST.RECORD.CASE@EXAMPLE.COM',
}, {
'email_from': 'test.record.weird@example.com Weird Format',
}, {
'email_from': 'Weird Format2 test.record.weird.2@example.com',
}
])
test_records = (
record_p_mult + record_p_fmt + record_p_unic +
record_p_case + record_p_weird + record_p_weird_2 +
record_mult + record_fmt + record_unic +
record_case + recod_weird + record_weird_2
)
mailing = self.env['mailing.mailing'].create({
'body_html': """<div><p>Hello ${object.name}</p>""",
'mailing_domain': [('id', 'in', test_records.ids)],
'mailing_model_id': self.env['ir.model']._get_id(dst_model),
'mailing_type': 'mail',
'name': 'SourceName',
'preview': 'Hi ${object.name} :)',
'reply_to_mode': 'update',
'subject': 'MailingSubject',
})
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
# Difference in email, email_to_recipients and email_to_mail
# -> email: trace email: normalized, to ease its management, mainly technical
# -> email_to_mail: mail.mail email: email_to stored in outgoing mail.mail (can be multi)
# -> email_to_recipients: email_to for outgoing emails, list means several recipients
self.assertMailTraces(
[
{'email': 'customer.multi.1@example.com, "Test Multi 2" <customer.multi.2@example.com>',
'email_to_recipients': [[f'"{customer_mult.name}" <customer.multi.1@example.com>', f'"{customer_mult.name}" <customer.multi.2@example.com>']],
'failure_type': False,
'partner': customer_mult,
'trace_status': 'sent'},
{'email': '"Formatted Customer" <test.customer.format@example.com>',
# mail to avoids double encapsulation
'email_to_recipients': [[f'"{customer_fmt.name}" <test.customer.format@example.com>']],
'failure_type': False,
'partner': customer_fmt,
'trace_status': 'sent'},
{'email': '"Unicode Customer" <test.customer.😊@example.com>',
# mail to avoids double encapsulation
'email_to_recipients': [[f'"{customer_unic.name}" <test.customer.😊@example.com>']],
'failure_type': False,
'partner': customer_unic,
'trace_status': 'sent'},
{'email': 'TEST.CUSTOMER.CASE@EXAMPLE.COM',
'email_to_recipients': [[f'"{customer_case.name}" <test.customer.case@example.com>']],
'failure_type': False,
'partner': customer_case,
'trace_status': 'sent'}, # lower cased
{'email': 'test.customer.weird@example.com Weird Format',
'email_to_recipients': [[f'"{customer_weird.name}" <test.customer.weird@example.comweirdformat>']],
'failure_type': False,
'partner': customer_weird,
'trace_status': 'sent'}, # concatenates everything after domain
{'email': 'Weird Format2 test.customer.weird.2@example.com',
'email_to_recipients': [[f'"{customer_weird_2.name}" <test.customer.weird.2@example.com>']],
'failure_type': False,
'partner': customer_weird_2,
'trace_status': 'sent'},
{'email': 'record.multi.1@example.com',
'email_to_mail': 'record.multi.1@example.com,record.multi.2@example.com',
'email_to_recipients': [['record.multi.1@example.com', 'record.multi.2@example.com']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'record.format@example.com',
'email_to_mail': 'record.format@example.com',
'email_to_recipients': [['record.format@example.com']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'record.😊@example.com',
'email_to_mail': 'record.😊@example.com',
'email_to_recipients': [['record.😊@example.com']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'test.record.case@example.com',
'email_to_mail': 'test.record.case@example.com',
'email_to_recipients': [['test.record.case@example.com']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'test.record.weird@example.comweirdformat',
'email_to_mail': 'test.record.weird@example.comweirdformat',
'email_to_recipients': [['test.record.weird@example.comweirdformat']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'test.record.weird.2@example.com',
'email_to_mail': 'test.record.weird.2@example.com',
'email_to_recipients': [['test.record.weird.2@example.com']],
'failure_type': False,
'trace_status': 'sent'},
],
mailing,
test_records,
check_mail=True,
)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_reply_to_mode_new(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.blacklist', count=5)
self.assertEqual(len(recipients), 5)
initial_messages = recipients.message_ids
mailing.write({
'mailing_domain': [('id', 'in', recipients.ids)],
'keep_archives': False,
'reply_to_mode': 'new',
'reply_to': self.test_alias.display_name,
})
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
answer_rec = self.gateway_mail_reply_wemail(MAIL_TEMPLATE, recipients[0].email_normalized, target_model=self.test_alias.alias_model_id.model)
self.assertTrue(bool(answer_rec))
self.assertEqual(answer_rec.name, 'Re: %s' % mailing.subject)
self.assertEqual(
answer_rec.message_ids.subject, 'Re: %s' % mailing.subject,
'Answer should be logged')
self.assertEqual(recipients.message_ids, initial_messages)
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5, opened=1, replied=1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_reply_to_mode_update(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.blacklist', count=5)
self.assertEqual(len(recipients), 5)
mailing.write({
'mailing_domain': [('id', 'in', recipients.ids)],
'keep_archives': False,
'reply_to_mode': 'update',
'reply_to': self.test_alias.display_name,
})
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
answer_rec = self.gateway_mail_reply_wemail(MAIL_TEMPLATE, recipients[0].email_normalized, target_model=self.test_alias.alias_model_id.model)
self.assertFalse(bool(answer_rec))
self.assertEqual(
recipients[0].message_ids[1].subject, mailing.subject,
'Should have keep a log (to enable thread-based answer)')
self.assertEqual(
recipients[0].message_ids[0].subject, 'Re: %s' % mailing.subject,
'Answer should be logged')
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5, opened=1, replied=1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_mailing_trace_utm(self):
""" Test mailing UTMs are caught on reply"""
self._create_mailing_list()
self.test_alias.write({
'alias_model_id': self.env['ir.model']._get('mailing.test.utm').id
})
source = self.env['utm.source'].create({'name': 'Source test'})
medium = self.env['utm.medium'].create({'name': 'Medium test'})
campaign = self.env['utm.campaign'].create({'name': 'Campaign test'})
subject = 'MassMailingTestUTM'
mailing = self.env['mailing.mailing'].create({
'name': 'UTMTest',
'subject': subject,
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'reply_to_mode': 'new',
'reply_to': '%s@%s' % (self.test_alias.alias_name, self.test_alias.alias_domain),
'keep_archives': True,
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [(4, self.mailing_list_1.id)],
'source_id': source.id,
'medium_id': medium.id,
'campaign_id': campaign.id
})
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
traces = self.env['mailing.trace'].search([('model', '=', self.mailing_list_1.contact_ids._name), ('res_id', 'in', self.mailing_list_1.contact_ids.ids)])
self.assertEqual(len(traces), 3)
# simulate response to mailing
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[1], use_in_reply_to=False)
mailing_test_utms = self.env['mailing.test.utm'].search([('name', '=', 'Re: %s' % subject)])
self.assertEqual(len(mailing_test_utms), 2)
for test_utm in mailing_test_utms:
self.assertEqual(test_utm.campaign_id, campaign)
self.assertEqual(test_utm.source_id, source)
self.assertEqual(test_utm.medium_id, medium)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_blacklist(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(count=5)
# blacklist records 2, 3, 4
self.env['mail.blacklist'].create({'email': recipients[2].email_normalized})
self.env['mail.blacklist'].create({'email': recipients[3].email_normalized})
self.env['mail.blacklist'].create({'email': recipients[4].email_normalized})
# unblacklist record 2
self.env['mail.blacklist'].action_remove_with_reason(
recipients[2].email_normalized, "human error"
)
self.env['mail.blacklist'].flush_model(['active'])
mailing.write({'mailing_domain': [('id', 'in', recipients.ids)]})
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com'},
{'email': 'test.record.01@test.example.com'},
{'email': 'test.record.02@test.example.com'},
{'email': 'test.record.03@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'},
{'email': 'test.record.04@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, recipients, check_mail=True
)
self.assertEqual(mailing.canceled, 2)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_blacklist_nomixin(self):
"""Test that blacklist is applied even if the target model doesn't inherit
from mail.thread.blacklist."""
test_records = self._create_mailing_test_records(model='mailing.test.simple', count=2)
self.mailing_bl.write({
'mailing_domain': [('id', 'in', test_records.ids)],
'mailing_model_id': self.env['ir.model']._get('mailing.test.simple').id,
})
self.env['mail.blacklist'].create([{
'email': test_records[0].email_from,
'active': True,
}])
with self.mock_mail_gateway(mail_unlink_sent=False):
self.mailing_bl.action_send_mail()
self.assertMailTraces([
{'email': email_normalize(test_records[0].email_from), 'trace_status': 'cancel', 'failure_type': 'mail_bl'},
{'email': email_normalize(test_records[1].email_from), 'trace_status': 'sent'},
], self.mailing_bl, test_records, check_mail=False)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_opt_out(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.optout', count=5)
# optout records 0 and 1
(recipients[0] | recipients[1]).write({'opt_out': True})
# blacklist records 4
self.env['mail.blacklist'].create({'email': recipients[4].email_normalized})
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout'),
'mailing_domain': [('id', 'in', recipients.ids)]
})
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'},
{'email': 'test.record.01@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'},
{'email': 'test.record.02@test.example.com'},
{'email': 'test.record.03@test.example.com'},
{'email': 'test.record.04@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, recipients, check_mail=True
)
self.assertEqual(mailing.canceled, 3)
@users('user_marketing')
def test_mailing_w_seenlist(self):
"""
Tests whether function `_get_seen_list` is correctly able to identify duplicate emails,
even through different batches.
Mails use different names to make sure they are recognized as duplicates even without being
normalized (e.g.: '"jc" <0@example.com>' and '"vd" <0@example.com>' are duplicates)
"""
BATCH_SIZE = 5
names = ['jc', 'vd']
emails = [f'test.{i}@example.com' for i in range(BATCH_SIZE)]
records = self.env['mailing.test.partner'].create([{
'name': f'test_duplicates {i}', 'email_from': f'"{names[i % 2]}" <{emails[i % BATCH_SIZE]}>'
} for i in range(20)])
mailing = self.env['mailing.mailing'].create({
'mailing_domain': [('name', 'ilike', 'test_duplicates %')],
'mailing_model_id': self.env.ref('test_mass_mailing.model_mailing_test_partner').id,
'name': 'test duplicates',
'subject': 'test duplicates',
})
with self.mock_mail_gateway():
for i in range(0, 20, BATCH_SIZE):
mailing.action_send_mail(records[i:i + BATCH_SIZE].mapped('id'))
self.assertEqual(len(self._mails), BATCH_SIZE)
self.assertEqual(mailing.canceled, 15)
mails_sent = [email_normalize(mail['email_to'][0]) for mail in self._mails]
for email in emails:
self.assertEqual(mails_sent.count(email), 1)
@users('user_marketing')
def test_mailing_w_seenlist_unstored_partner(self):
""" Test seen list when partners are not stored. """
test_customers = self.env['res.partner'].sudo().create([
{'email': f'"Mailing Partner {idx}" <email.from.{idx}@test.example.com',
'name': f'Mailing Partner {idx}',
} for idx in range(8)
])
test_records = self.env['mailing.test.partner.unstored'].create([
{'email_from': f'email.from.{idx}@test.example.com',
'name': f'Mailing Record {idx}',
} for idx in range(10)
])
self.assertEqual(test_records[:8].partner_id, test_customers)
self.assertFalse(test_records[9:].partner_id)
mailing = self.env['mailing.mailing'].create({
'body_html': '<p>Marketing stuff for ${object.name}</p>',
'mailing_domain': [('id', 'in', test_records.ids)],
'mailing_model_id': self.env['ir.model']._get_id('mailing.test.partner.unstored'),
'name': 'test',
'subject': 'Blacklisted',
})
# create existing traces to check the seen list
traces = self._create_sent_traces(
mailing,
test_records[:3]
)
traces.flush_model()
# check remaining recipients effectively check seen list
mailing.action_put_in_queue()
res_ids = mailing._get_remaining_recipients()
self.assertEqual(sorted(res_ids), sorted(test_records[3:].ids))
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertEqual(len(self._mails), 7, 'Mailing: seen list should contain 3 existing traces')
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_mailing_list_optout(self):
""" Test mailing list model specific optout behavior """
mailing_contact_1 = self.env['mailing.contact'].create({'name': 'test 1A', 'email': 'test@test.example.com'})
mailing_contact_2 = self.env['mailing.contact'].create({'name': 'test 1B', 'email': 'test@test.example.com'})
mailing_contact_3 = self.env['mailing.contact'].create({'name': 'test 3', 'email': 'test3@test.example.com'})
mailing_contact_4 = self.env['mailing.contact'].create({'name': 'test 4', 'email': 'test4@test.example.com'})
mailing_contact_5 = self.env['mailing.contact'].create({'name': 'test 5', 'email': 'test5@test.example.com'})
# create mailing list record
mailing_list_1 = self.env['mailing.list'].create({
'name': 'A',
'contact_ids': [
(4, mailing_contact_1.id),
(4, mailing_contact_2.id),
(4, mailing_contact_3.id),
(4, mailing_contact_5.id),
]
})
mailing_list_2 = self.env['mailing.list'].create({
'name': 'B',
'contact_ids': [
(4, mailing_contact_3.id),
(4, mailing_contact_4.id),
]
})
# contact_1 is optout but same email is not optout from the same list
# contact 3 is optout in list 1 but not in list 2
# contact 5 is optout
subs = self.env['mailing.contact.subscription'].search([
'|', '|',
'&', ('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id),
'&', ('contact_id', '=', mailing_contact_3.id), ('list_id', '=', mailing_list_1.id),
'&', ('contact_id', '=', mailing_contact_5.id), ('list_id', '=', mailing_list_1.id)
])
subs.write({'opt_out': True})
# create mass mailing record
mailing = self.env['mailing.mailing'].create({
'name': 'SourceName',
'subject': 'MailingSubject',
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [(4, ml.id) for ml in mailing_list_1 | mailing_list_2],
})
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test@test.example.com', 'trace_status': 'sent'},
{'email': 'test@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_dup'},
{'email': 'test3@test.example.com'},
{'email': 'test4@test.example.com'},
{'email': 'test5@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'}],
mailing,
mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
check_mail=True
)
self.assertEqual(mailing.canceled, 2)

View file

@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
@tagged('mass_mailing')
class TestMassMailingServer(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMassMailingServer, cls).setUpClass()
cls._init_mail_servers()
cls.recipients = cls._create_mailing_test_records(model='mailing.test.optout', count=8)
def test_mass_mailing_server_archived_usage_protection(self):
""" Test the protection against using archived server:
- servers used cannot be archived
- mailing clone of a mailing with an archived server gets the default one instead
"""
servers = self.env['ir.mail_server'].create([{
'name': 'Server 1',
'smtp_host': 'archive-test1.smtp.local',
}, {
'name': 'Server 2',
'smtp_host': 'archive-test2.smtp.local',
}])
self.env['ir.config_parameter'].set_param('mass_mailing.mail_server_id', servers[0].id)
mailing = self.env['mailing.mailing'].create({
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'specific_user@test.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
})
mailing_clone = mailing.copy()
self.assertEqual(mailing_clone.mail_server_id.id, servers[0].id,
'The clone of a mailing inherits from the server of the copied mailing')
with self.assertRaises(UserError, msg='Servers still used as default and for 2 mailings'):
servers.action_archive()
self.assertTrue(all(server.active for server in servers), 'All servers must be active')
self.env['ir.config_parameter'].set_param('mass_mailing.mail_server_id', False)
with self.assertRaises(UserError, msg='Servers still used for 2 mailings'):
servers.action_archive()
self.assertTrue(all(server.active for server in servers), 'All servers must be active')
with self.mock_smtplib_connection():
mailing.action_send_mail()
with self.assertRaises(UserError, msg='Servers still used for 1 mailings'):
servers.action_archive()
self.assertTrue(all(server.active for server in servers), 'All servers must be active')
with self.mock_smtplib_connection():
mailing_clone.action_send_mail()
servers.action_archive() # Servers no more used -> no error
self.assertFalse(servers.filtered('active'), 'All servers must be archived')
self.assertFalse(mailing.copy().mail_server_id,
'The clone of a mailing with an archived server gets the default one (none here)')
servers[1].action_unarchive()
self.env['ir.config_parameter'].set_param('mass_mailing.mail_server_id', servers[1].id)
mailing_clone = mailing.copy()
self.assertEqual(mailing_clone.mail_server_id.id, servers[1].id,
'The clone of a mailing with an archived server gets the default one')
mailing_clone.action_archive()
with self.assertRaises(UserError, msg='Servers still used as default'):
servers.action_archive()
self.assertTrue(servers[1].active)
self.env['ir.config_parameter'].set_param('mass_mailing.mail_server_id', False)
servers.action_archive() # Servers no more used -> no error
self.assertFalse(servers.filtered('active'), 'All servers must be archived')
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing')
def test_mass_mailing_server_batch(self):
"""Test that the right mail server is chosen to send the mailing.
Test also the envelop and the SMTP headers.
"""
# Test sending mailing in batch
mailings = self.env['mailing.mailing'].create([{
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'specific_user@test.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
}, {
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'unknown_name@test.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
}])
with self.mock_smtplib_connection():
mailings.action_send_mail()
self.assertEqual(self.find_mail_server_mocked.call_count, 2, 'Must be called only once per mail from')
self.assert_email_sent_smtp(
smtp_from='specific_user@test.com',
message_from='specific_user@test.com',
from_filter=self.server_user.from_filter,
emails_count=8,
)
self.assert_email_sent_smtp(
# Must use the bounce address here because the mail server
# is configured for the entire domain "test.com"
smtp_from=lambda x: 'bounce' in x,
message_from='unknown_name@test.com',
from_filter=self.server_domain.from_filter,
emails_count=8,
)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing')
def test_mass_mailing_server_default(self):
# We do not have a mail server for this address email, so fall back to the
# "notifications@domain" email.
mailings = self.env['mailing.mailing'].create([{
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': '"Testing" <unknow_email@unknow_domain.com>',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
}])
with self.mock_smtplib_connection():
mailings.action_send_mail()
self.assertEqual(self.find_mail_server_mocked.call_count, 1)
self.assert_email_sent_smtp(
smtp_from='notifications@test.com',
message_from='"Testing" <notifications@test.com>',
from_filter=self.server_notification.from_filter,
emails_count=8,
)
self.assertEqual(self.find_mail_server_mocked.call_count, 1, 'Must be called only once')
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing')
def test_mass_mailing_server_forced(self):
# We force a mail server on one mailing
mailings = self.env['mailing.mailing'].create([{
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': self.server_user.from_filter,
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
}, {
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'unknow_email@unknow_domain.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
'mail_server_id': self.server_notification.id,
}])
with self.mock_smtplib_connection():
mailings.action_send_mail()
self.assertEqual(self.find_mail_server_mocked.call_count, 1, 'Must not be called when mail server is forced')
self.assert_email_sent_smtp(
smtp_from='specific_user@test.com',
message_from='specific_user@test.com',
from_filter=self.server_user.from_filter,
emails_count=8,
)
self.assert_email_sent_smtp(
smtp_from='unknow_email@unknow_domain.com',
message_from='unknow_email@unknow_domain.com',
from_filter=self.server_notification.from_filter,
emails_count=8,
)

View file

@ -0,0 +1,361 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.test_mass_mailing.tests.common import TestMassSMSCommon
from odoo import exceptions
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
@tagged('mass_mailing', 'mass_mailing_sms')
class TestMassSMSInternals(TestMassSMSCommon):
@users('user_marketing')
def test_mass_sms_domain(self):
mailing = self.env['mailing.mailing'].create({
'name': 'Xmas Spam',
'subject': 'Xmas Spam',
'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
'mailing_type': 'sms',
})
self.assertEqual(literal_eval(mailing.mailing_domain), [])
mailing = self.env['mailing.mailing'].create({
'name': 'Xmas Spam',
'subject': 'Xmas Spam',
'mailing_model_id': self.env['ir.model']._get('mail.test.sms.bl').id,
'mailing_type': 'sms',
})
self.assertEqual(literal_eval(mailing.mailing_domain), [('phone_sanitized_blacklisted', '=', False)])
@users('user_marketing')
def test_mass_sms_internals(self):
with self.with_user('user_marketing'):
mailing = self.env['mailing.mailing'].create({
'name': 'Xmas Spam',
'subject': 'Xmas Spam',
'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
'mailing_type': 'sms',
'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
'sms_template_id': self.sms_template.id,
'sms_allow_unsubscribe': False,
})
self.assertEqual(mailing.mailing_model_real, 'mail.test.sms')
self.assertEqual(mailing.medium_id, self.env.ref('mass_mailing_sms.utm_medium_sms'))
self.assertEqual(mailing.body_plaintext, self.sms_template.body)
remaining_res_ids = mailing._get_remaining_recipients()
self.assertEqual(set(remaining_res_ids), set(self.records.ids))
with self.mockSMSGateway():
mailing.action_send_sms()
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': self.records_numbers[i],
'content': 'Dear %s this is a mass SMS.' % record.display_name
} for i, record in enumerate(self.records)],
mailing, self.records,
)
@users('user_marketing')
def test_mass_sms_internals_errors(self):
# same customer, specific different number on record -> should be valid
new_record_1 = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_nr1',
'customer_id': self.partners[0].id,
'phone_nbr': '0456999999',
})
void_record = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_void',
'customer_id': False,
'phone_nbr': '',
})
falsy_record_1 = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_falsy_1',
'customer_id': False,
'phone_nbr': 'abcd',
})
falsy_record_2 = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_falsy_2',
'customer_id': False,
'phone_nbr': '04561122',
})
bl_record_1 = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_bl_1',
'customer_id': False,
'phone_nbr': '0456110011',
})
self.env['phone.blacklist'].sudo().create({'number': '0456110011'})
# new customer, number already on record -> should be ignored
country_be_id = self.env.ref('base.be').id
nr2_partner = self.env['res.partner'].create({
'name': 'Partner_nr2',
'country_id': country_be_id,
'mobile': '0456449999',
})
new_record_2 = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_nr2',
'customer_id': nr2_partner.id,
'phone_nbr': self.records[0].phone_nbr,
})
records_numbers = self.records_numbers + ['+32456999999']
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({'sms_force_send': False}) # force outgoing sms, not sent
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing.action_send_sms()
self.assertSMSTraces(
[{'partner': record.customer_id, 'number': records_numbers[i],
'content': 'Dear %s this is a mass SMS' % record.display_name}
for i, record in enumerate(self.records | new_record_1)],
mailing, self.records | new_record_1,
)
# duplicates
self.assertSMSTraces(
[{'partner': new_record_2.customer_id, 'number': self.records_numbers[0],
'content': 'Dear %s this is a mass SMS' % new_record_2.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_duplicate'}],
mailing, new_record_2,
)
# blacklist
self.assertSMSTraces(
[{'partner': self.env['res.partner'], 'number': phone_validation.phone_format(bl_record_1.phone_nbr, 'BE', '32', force_format='E164'),
'content': 'Dear %s this is a mass SMS' % bl_record_1.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_blacklist'}],
mailing, bl_record_1,
)
# missing number
self.assertSMSTraces(
[{'partner': self.env['res.partner'], 'number': False,
'content': 'Dear %s this is a mass SMS' % void_record.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_number_missing'}],
mailing, void_record,
)
# wrong values
self.assertSMSTraces(
[{'partner': self.env['res.partner'], 'number': record.phone_nbr,
'content': 'Dear %s this is a mass SMS' % record.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_number_format'}
for record in falsy_record_1 + falsy_record_2],
mailing, falsy_record_1 + falsy_record_2,
)
@users('user_marketing')
def test_mass_sms_internals_done_ids(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({'sms_force_send': False}) # check with outgoing traces, not already sent
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing.action_send_sms(res_ids=self.records[:5].ids)
traces = self.env['mailing.trace'].search([('mass_mailing_id', 'in', mailing.ids)])
self.assertEqual(len(traces), 5)
# new traces generated
self.assertSMSTraces(
[{'partner': record.customer_id, 'number': self.records_numbers[i],
'content': 'Dear %s this is a mass SMS' % record.display_name}
for i, record in enumerate(self.records[:5])],
mailing, self.records[:5],
)
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing.action_send_sms(res_ids=self.records.ids)
# delete old traces (for testing purpose: ease check by deleting old ones)
traces.unlink()
# new failed traces generated for duplicates
self.assertSMSTraces(
[{'partner': record.customer_id, 'number': self.records_numbers[i],
'content': 'Dear %s this is a mass SMS' % record.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_duplicate'}
for i, record in enumerate(self.records[:5])],
mailing, self.records[:5],
)
# new traces generated
self.assertSMSTraces(
[{'partner': record.customer_id, 'number': self.records_numbers[i+5],
'content': 'Dear %s this is a mass SMS' % record.display_name}
for i, record in enumerate(self.records[5:])],
mailing, self.records[5:],
)
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mass_sms_test_button(self):
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject {{ object.name }}',
'preview': 'Preview {{ object.name }}',
'state': 'draft',
'mailing_type': 'sms',
'body_plaintext': 'Hello {{ object.name }}',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
mailing_test = self.env['mailing.sms.test'].with_user(self.user_marketing).create({
'numbers': '+32456001122',
'mailing_id': mailing.id,
})
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing_test.action_send_sms()
# Test if bad inline_template in the body raises an error
mailing.write({
'body_plaintext': 'Hello {{ object.name_id.id }}',
})
with self.with_user('user_marketing'):
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.action_send_sms()
@tagged('mass_mailing', 'mass_mailing_sms')
class TestMassSMS(TestMassSMSCommon):
@users('user_marketing')
def test_mass_sms_links(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({
'body_plaintext': 'Dear {{ object.display_name }} this is a mass SMS with two links http://www.odoo.com/smstest and http://www.odoo.com/smstest/{{ object.name }}',
'sms_template_id': False,
'sms_force_send': True,
'sms_allow_unsubscribe': True,
})
with self.mockSMSGateway():
mailing.action_send_sms()
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': self.records_numbers[i],
'trace_status': 'sent',
'content': 'Dear %s this is a mass SMS with two links' % record.display_name
} for i, record in enumerate(self.records)],
mailing, self.records,
sms_links_info=[[
('http://www.odoo.com/smstest', True, {}),
('http://www.odoo.com/smstest/%s' % record.name, True, {}),
# unsubscribe is not shortened and parsed at sending
('unsubscribe', False, {}),
] for record in self.records],
)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mass_sms_partner_only(self):
""" Check sending SMS marketing on models having only a partner_id fields
set is working. """
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mail.test.sms.partner').id,
})
records = self.env['mail.test.sms.partner'].create([
{'name': 'MassSMSTest on %s' % partner.name,
'customer_id': partner.id,
} for partner in self.partners
])
with self.mockSMSGateway():
mailing.action_send_sms()
self.assertEqual(len(mailing.mailing_trace_ids), 10)
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': record.customer_id.phone_sanitized,
'trace_status': 'sent',
'content': 'Dear %s this is a mass SMS with two links' % record.display_name
} for record in records],
mailing, records,
sms_links_info=[[
('http://www.odoo.com/smstest', True, {}),
('http://www.odoo.com/smstest/%s' % record.id, True, {}),
# unsubscribe is not shortened and parsed at sending
('unsubscribe', False, {}),
] for record in records],
)
# add a new record, send -> sent list should not resend traces
new_record = self.env['mail.test.sms.partner'].create([
{'name': 'Duplicate SMS on %s' % self.partners[0].name,
'customer_id': self.partners[0].id,
}
])
with self.mockSMSGateway():
mailing.action_send_sms()
self.assertEqual(len(mailing.mailing_trace_ids), 11)
self.assertSMSTraces(
[{'partner': new_record.customer_id,
'number': new_record.customer_id.phone_sanitized,
'trace_status': 'sent',
'content': 'Dear %s this is a mass SMS with two links' % new_record.display_name
}],
mailing, new_record,
sms_links_info=[[
('http://www.odoo.com/smstest', True, {}),
('http://www.odoo.com/smstest/%s' % new_record.id, True, {}),
# unsubscribe is not shortened and parsed at sending
('unsubscribe', False, {}),
]],
)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mass_sms_partner_only_m2m(self):
""" Check sending SMS marketing on models having only a m2m to partners
is currently not suppored. """
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mail.test.sms.partner.2many').id,
})
self.env['mail.test.sms.partner.2many'].create([
{'name': 'MassSMSTest on %s' % partner.name,
'customer_ids': [(4, partner.id)],
} for partner in self.partners
])
with self.assertRaises(exceptions.UserError), self.mockSMSGateway():
mailing.action_send_sms()
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mass_sms_w_opt_out(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
recipients = self._create_mailing_sms_test_records(model='mail.test.sms.bl.optout', count=5)
# optout records 0 and 1
(recipients[0] | recipients[1]).write({'opt_out': True})
# blacklist records 4
# TDE FIXME: sudo should not be necessary
self.env['phone.blacklist'].sudo().create({'number': recipients[4].phone_nbr})
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mail.test.sms.bl.optout'),
'mailing_domain': [('id', 'in', recipients.ids)],
})
with self.mockSMSGateway():
mailing.action_send_sms()
self.assertSMSTraces(
[{'number': '+32456000000', 'trace_status': 'cancel', 'failure_type': 'sms_optout'},
{'number': '+32456000101', 'trace_status': 'cancel', 'failure_type': 'sms_optout'},
{'number': '+32456000202', 'trace_status': 'sent'},
{'number': '+32456000303', 'trace_status': 'sent'},
{'number': '+32456000404', 'trace_status': 'cancel', 'failure_type': 'sms_blacklist'}],
mailing, recipients
)
self.assertEqual(mailing.canceled, 3)

View file

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import html
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mass_mailing.data.mail_test_data import MAIL_TEMPLATE
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.tests.common import users
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('digest', 'mass_mailing')
class TestMailingStatistics(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMailingStatistics, cls).setUpClass()
cls.user_marketing_2 = mail_new_test_user(
cls.env,
groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
login='user_marketing_2',
name='Marie Marketing',
signature='--\nMarie'
)
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing.models.mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
def test_mailing_statistics(self):
target_records = self._create_mailing_test_records(model='mailing.test.blacklist', count=11)
target_records[10]['email_from'] = False # void email should lead to a 'cancel' trace_status
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': self.user_marketing_2.id})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
# simulate some replies and clicks
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[1], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[2], use_in_reply_to=True)
self.gateway_mail_click(mailing, target_records[0], 'https://www.odoo.be')
self.gateway_mail_click(mailing, target_records[2], 'https://www.odoo.be')
self.gateway_mail_click(mailing, target_records[3], 'https://www.odoo.be')
# check mailing statistics
self.assertEqual(mailing.canceled, 1)
self.assertEqual(mailing.clicked, 3)
self.assertEqual(mailing.clicks_ratio, 30)
self.assertEqual(mailing.delivered, 10)
self.assertEqual(mailing.opened, 4)
self.assertEqual(mailing.opened_ratio, 40)
self.assertEqual(mailing.replied, 3)
self.assertEqual(mailing.replied_ratio, 30)
self.assertEqual(mailing.sent, 10)
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing._action_send_statistics()
self.assertEqual(len(self._new_mails), 1, "Mailing: a mail should have been created for statistics")
mail = self._new_mails[0]
# test email values
self.assertEqual(mail.author_id, self.user_marketing_2.partner_id)
self.assertEqual(mail.email_from, self.user_marketing_2.email_formatted)
self.assertEqual(mail.email_to, self.user_marketing_2.email_formatted)
self.assertEqual(mail.reply_to, self.company_admin.partner_id.email_formatted)
self.assertEqual(mail.state, 'outgoing')
# test body content: KPIs
body_html = html.fromstring(mail.body_html)
kpi_values = body_html.xpath('//div[@data-field="mail"]//*[hasclass("kpi_value")]/text()')
self.assertEqual(
[t.strip().strip('%') for t in kpi_values],
['100', str(mailing.opened_ratio), str(mailing.replied_ratio)]
)
# test body content: clicks (a bit hackish but hey we are in stable)
kpi_click_values = body_html.xpath('//div[hasclass("global_layout")]/table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
first_link_value = int(kpi_click_values[0].strip().split()[1].strip('()'))
self.assertEqual(first_link_value, mailing.clicked)
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing.models.mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
def test_mailing_statistics_wo_user(self):
target_records = self._create_mailing_test_records(model='mailing.test.blacklist', count=10)
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': False})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._action_send_statistics()
self.assertEqual(len(self._new_mails), 1, "Mailing: a mail should have been created for statistics")
mail = self._new_mails[0]
# test email values
self.assertEqual(mail.author_id, self.user_marketing.partner_id)
self.assertEqual(mail.email_from, self.user_marketing.email_formatted)
self.assertEqual(mail.email_to, self.user_marketing.email_formatted)
self.assertEqual(mail.reply_to, self.company_admin.partner_id.email_formatted)
self.assertEqual(mail.state, 'outgoing')

View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import html
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mass_mailing.tests.common import TestMassSMSCommon
from odoo.tests.common import users
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('digest', 'mass_mailing', 'mass_mailing_sms')
class TestMailingStatistics(TestMassSMSCommon):
@classmethod
def setUpClass(cls):
super(TestMailingStatistics, cls).setUpClass()
cls.user_marketing_2 = mail_new_test_user(
cls.env,
groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
login='user_marketing_2',
name='Marie Marketing',
signature='--\nMarie'
)
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing_sms.models.mailing_mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
def test_mailing_statistics_sms(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
target_records = self.env['mail.test.sms'].browse(self.records.ids)
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': self.user_marketing_2.id})
mailing.action_put_in_queue()
with self.mockSMSGateway():
mailing.action_send_sms()
# simulate some replies and clicks
self.gateway_sms_click(mailing, target_records[0])
self.gateway_sms_click(mailing, target_records[2])
self.gateway_sms_click(mailing, target_records[3])
# check mailing statistics
self.assertEqual(mailing.clicked, 3)
self.assertEqual(mailing.delivered, 10)
self.assertEqual(mailing.opened, 3)
self.assertEqual(mailing.opened_ratio, 30)
self.assertEqual(mailing.sent, 10)
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing._action_send_statistics()
self.assertEqual(len(self._new_mails), 1, "Mailing: a mail should have been created for statistics")
mail = self._new_mails[0]
# test email values
self.assertEqual(mail.author_id, self.user_marketing_2.partner_id)
self.assertEqual(mail.email_from, self.user_marketing_2.email_formatted)
self.assertEqual(mail.email_to, self.user_marketing_2.email_formatted)
self.assertEqual(mail.reply_to, self.company_admin.partner_id.email_formatted)
self.assertEqual(mail.state, 'outgoing')
# test body content: KPIs
body_html = html.fromstring(mail.body_html)
kpi_values = body_html.xpath('//div[@data-field="sms"]//*[hasclass("kpi_value")]/text()')
self.assertEqual(
[t.strip().strip('%') for t in kpi_values],
['100', str(mailing.clicks_ratio), str(mailing.bounced_ratio)]
)
# test body content: clicks (a bit hackish but hey we are in stable)
kpi_click_values = body_html.xpath('//div[hasclass("global_layout")]/table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
first_link_value = int(kpi_click_values[0].strip().split()[1].strip('()'))
self.assertEqual(first_link_value, mailing.clicked)

View file

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import lxml.html
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.fields import Command
from odoo.tests.common import users, tagged
from odoo.tools import mute_logger
@tagged('mailing_manage')
class TestMailingTest(TestMassMailCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mailing_test_button(self):
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject {{ object.name }}',
'preview': 'Preview {{ object.name }}',
'state': 'draft',
'mailing_type': 'mail',
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
'mass_mailing_id': mailing.id,
})
with self.mock_mail_gateway():
mailing_test.send_mail_test()
# not great but matches send_mail_test, maybe that should be a method
# on mailing_test?
record = self.env[mailing.mailing_model_real].search([], limit=1)
first_child = lxml.html.fromstring(self._mails.pop()['body']).xpath('//body/*[1]')[0]
self.assertEqual(first_child.tag, 'div')
self.assertIn('display:none', first_child.get('style'),
"the preview node should be hidden")
self.assertEqual(first_child.text.strip(), "Preview " + record.name,
"the preview node should contain the preview text")
# Test if bad inline_template in the subject raises an error
mailing.write({'subject': 'Subject {{ object.name_id.id }}'})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
# Test if bad inline_template in the body raises an error
mailing.write({
'subject': 'Subject {{ object.name }}',
'body_html': '<p>Hello <t t-out="object.name_id.id"/></p>',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
# Test if bad inline_template in the preview raises an error
mailing.write({
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'preview': 'Preview {{ object.name_id.id }}',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
def test_mailing_test_equals_reality(self):
"""
Check that both test and real emails will format the qweb and inline placeholders correctly in body and subject.
"""
self.env['mailing.contact'].search([]).unlink()
contact_list = self.env['mailing.list'].create({
'name': 'Testers',
'contact_ids': [Command.create({
'name': 'Mitchell Admin',
'email': 'real@real.com',
})],
})
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject {{ object.name }} <t t-out="object.name"/>',
'state': 'draft',
'mailing_type': 'mail',
'body_html': '<p>Hello {{ object.name }} <t t-out="object.name"/></p>',
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [contact_list.id],
})
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
'mass_mailing_id': mailing.id,
})
with self.mock_mail_gateway():
mailing_test.send_mail_test()
expected_subject = 'Subject Mitchell Admin <t t-out="object.name"/>'
expected_body = 'Hello {{ object.name }} Mitchell Admin'
self.assertSentEmail(self.env.user.partner_id, ['test@test.com'],
subject=expected_subject,
body_content=expected_body)
with self.mock_mail_gateway():
# send the mailing
mailing.action_launch()
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').method_direct_trigger()
self.assertSentEmail(self.env.user.partner_id, ['real@real.com'],
subject=expected_subject,
body_content=expected_body)

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance
from odoo.tests.common import users, warmup
from odoo.tests import tagged
from odoo.tools import mute_logger
class TestMassMailPerformanceBase(BaseMailPerformance):
@classmethod
def setUpClass(cls):
super(TestMassMailPerformanceBase, cls).setUpClass()
cls.user_marketing = mail_new_test_user(
cls.env,
groups='base.group_user,mass_mailing.group_mass_mailing_user',
login='marketing',
name='Martial Marketing',
signature='--\nMartial'
)
@tagged('mail_performance', 'post_install', '-at_install')
class TestMassMailPerformance(TestMassMailPerformanceBase):
def setUp(self):
super(TestMassMailPerformance, self).setUp()
values = [{
'name': 'Recipient %s' % x,
'email_from': 'Recipient <rec.%s@example.com>' % x,
} for x in range(0, 50)]
self.mm_recs = self.env['mailing.performance'].create(values)
@users('__system__', 'marketing')
@warmup
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests')
def test_send_mailing(self):
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
'subject': 'Test',
'body_html': '<p>Hello <a role="button" href="https://www.example.com/foo/bar?baz=qux">quux</a><a role="button" href="/unsubscribe_from_list">Unsubscribe</a></p>',
'reply_to_mode': 'new',
'mailing_model_id': self.ref('test_mass_mailing.model_mailing_performance'),
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
# runbot needs +51 compared to local
with self.assertQueryCount(__system__=1473, marketing=1474):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
self.assertEqual(mailing.delivered, 50)
mails = self.env['mail.mail'].sudo().search([('mailing_id', '=', mailing.id)])
self.assertFalse(mails, 'Should have auto-deleted the <mail.mail>')
@tagged('mail_performance', 'post_install', '-at_install')
class TestMassMailBlPerformance(TestMassMailPerformanceBase):
def setUp(self):
""" In this setup we prepare 20 blacklist entries. We therefore add
20 recipients compared to first test in order to have comparable results. """
super(TestMassMailBlPerformance, self).setUp()
values = [{
'name': 'Recipient %s' % x,
'email_from': 'Recipient <rec.%s@example.com>' % x,
} for x in range(0, 62)]
self.mm_recs = self.env['mailing.performance.blacklist'].create(values)
for x in range(1, 13):
self.env['mail.blacklist'].create({
'email': 'rec.%s@example.com' % (x * 5)
})
self.env.flush_all()
@users('__system__', 'marketing')
@warmup
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests')
def test_send_mailing_w_bl(self):
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
'subject': 'Test',
'body_html': '<p>Hello <a role="button" href="https://www.example.com/foo/bar?baz=qux">quux</a><a role="button" href="/unsubscribe_from_list">Unsubscribe</a></p>',
'reply_to_mode': 'new',
'mailing_model_id': self.ref('test_mass_mailing.model_mailing_performance_blacklist'),
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
# runbot needs +51 compared to local
with self.assertQueryCount(__system__=1546, marketing=1547):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
self.assertEqual(mailing.delivered, 50)
cancelled_mail_count = self.env['mail.mail'].sudo().search([('mailing_id', '=', mailing.id)])
self.assertEqual(len(cancelled_mail_count), 12, 'Should not have auto deleted the blacklisted emails')

View file

@ -0,0 +1,85 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.tests import common
from odoo.tests import tagged, users
@tagged("utm")
class TestUTM(common.TestMassMailCommon):
@users("employee")
def test_utm_source_mixin_name(self):
""" Test name management with source mixin, as name should be unique
and automatically incremented """
sources = self.env["utm.test.source.mixin"].create([
{
'name': 'Test',
'title': 'Test',
}
for idx in range(5)]
)
self.assertListEqual(
sources.mapped('name'),
["Test", "Test [2]", "Test [3]", "Test [4]", "Test [5]"]
)
@users("employee")
def test_utm_source_mixin_name_brackets(self):
""" Test with brackets """
false_dupes = self.env["utm.test.source.mixin"].create([
{
'name': 'NewTest [2]',
'title': 'NewTest',
}
for idx in range(3)] + [
{
'name': 'NewTest [3]',
'title': 'NewTest',
}, {
'name': 'NewTest',
'title': 'NewTest',
}]
)
self.assertListEqual(
false_dupes.mapped('name'),
["NewTest [2]", "NewTest", "NewTest [3]", "NewTest [4]", "NewTest [5]"]
)
new_source = self.env["utm.test.source.mixin"].create({
"name": "OtherTest [2]",
})
self.assertEqual(new_source.name, "OtherTest [2]")
@users("employee")
def test_utm_source_mixin_name_cross_model(self):
""" Uniqueness of source should be ensured cross models. For this purpose
we use two models using the utm.source.mixin, allowing to check conflict
management between models. """
source_1 = self.env["utm.test.source.mixin"].create({
"name": "Test",
"title": "Test",
})
self.assertEqual(source_1.name, "Test")
self.assertEqual(source_1.title, "Test")
source_other_1 = self.env["utm.test.source.mixin.other"].create({
"name": "Test",
"title": "Test",
})
self.assertEqual(source_other_1.name, "Test [2]")
self.assertEqual(source_other_1.title, "Test")
source_other_2 = self.env["utm.test.source.mixin.other"].create({
"name": "New",
"title": "New",
})
self.assertEqual(source_other_2.name, "New")
self.assertEqual(source_other_2.title, "New")
source_other_2.write({"name": "Test"})
self.assertEqual(source_other_2.name, "Test [3]")
self.assertEqual(source_other_2.title, "New")
source_2 = source_1.copy()
self.assertEqual(source_2.name, "Test [4]")
self.assertEqual(source_2.title, "Test")