Hello ${object.name}
""",
'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"
',
'email_to_recipients': [[f'"{customer_mult.name}" ', f'"{customer_mult.name}" ']],
'failure_type': False,
'partner': customer_mult,
'trace_status': 'sent'},
{'email': '"Formatted Customer" ',
# mail to avoids double encapsulation
'email_to_recipients': [[f'"{customer_fmt.name}" ']],
'failure_type': False,
'partner': customer_fmt,
'trace_status': 'sent'},
{'email': '"Unicode Customer" ',
# mail to avoids double encapsulation
'email_to_recipients': [[f'"{customer_unic.name}" ']],
'failure_type': False,
'partner': customer_unic,
'trace_status': 'sent'},
{'email': 'TEST.CUSTOMER.CASE@EXAMPLE.COM',
'email_to_recipients': [[f'"{customer_case.name}" ']],
'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}" ']],
'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}" ']],
'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': 'Hello
',
'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']._remove(
recipients[2].email_normalized, message="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}" Marketing stuff for ${object.name}',
'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 """
# as duplication checks body and subject, we create 2 exact copies to make sure only 1 is sent
mailing_contact_1 = self.env['mailing.contact'].create({'name': 'test 1', 'email': 'test@test.example.com'})
mailing_contact_2 = self.env['mailing.contact'].create({'name': 'test 1', '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.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': 'Hello
',
'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,
mailing_contact_2 + mailing_contact_1 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
check_mail=True
)
self.assertEqual(mailing.canceled, 2)