# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import psycopg2 from unittest.mock import call from odoo import api from odoo.addons.base.tests.common import MockSmtplibCase from odoo.addons.test_mail.tests.common import TestMailCommon from odoo.tests import common, tagged from odoo.tools import mute_logger @tagged('mail_mail') class TestMailMail(TestMailCommon, MockSmtplibCase): @classmethod def setUpClass(cls): super(TestMailMail, cls).setUpClass() cls._init_mail_gateway() cls._init_mail_servers() cls.server_domain_2 = cls.env['ir.mail_server'].create({ 'name': 'Server 2', 'smtp_host': 'test_2.com', 'from_filter': 'test_2.com', }) cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({ 'name': 'Test', 'email_from': 'ignasse@example.com', }).with_context({}) @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_message_notify_from_mail_mail(self): # Due ot post-commit hooks, store send emails in every step mail = self.env['mail.mail'].sudo().create({ 'body_html': '

Test

', 'email_to': 'test@example.com', 'partner_ids': [(4, self.user_employee.partner_id.id)] }) with self.mock_mail_gateway(): mail.send() self.assertSentEmail(mail.env.user.partner_id, ['test@example.com']) self.assertEqual(len(self._mails), 1) def test_mail_mail_return_path(self): # mail without thread-enabled record base_values = { 'body_html': '

Test

', 'email_to': 'test@example.com', } mail = self.env['mail.mail'].create(base_values) with self.mock_mail_gateway(): mail.send() self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain)) # mail on thread-enabled record mail = self.env['mail.mail'].create(dict(base_values, **{ 'model': self.test_record._name, 'res_id': self.test_record.id, })) with self.mock_mail_gateway(): mail.send() self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain)) @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_mail_send_server(self): """Test that the mails are send in batch. Batch are defined by the mail server and the email from field. """ self.assertEqual(self.env['ir.mail_server']._get_default_from_address(), 'notifications@test.com') mail_values = { 'body_html': '

Test

', 'email_to': 'user@example.com', } # Should be encapsulated in the notification email mails = self.env['mail.mail'].create([{ **mail_values, 'email_from': 'test@unknown_domain.com', } for _ in range(5)]) | self.env['mail.mail'].create([{ **mail_values, 'email_from': 'test_2@unknown_domain.com', } for _ in range(5)]) # Should use the test_2 mail server # Once with "user_1@test_2.com" as login # Once with "user_2@test_2.com" as login mails |= self.env['mail.mail'].create([{ **mail_values, 'email_from': 'user_1@test_2.com', } for _ in range(5)]) | self.env['mail.mail'].create([{ **mail_values, 'email_from': 'user_2@test_2.com', } for _ in range(5)]) # Mail server is forced mails |= self.env['mail.mail'].create([{ **mail_values, 'email_from': 'user_1@test_2.com', 'mail_server_id': self.server_domain.id, } for _ in range(5)]) with self.mock_smtplib_connection(): mails.send() self.assertEqual(self.find_mail_server_mocked.call_count, 4, 'Must be called only once per "mail from" when the mail server is not forced') self.assertEqual(len(self.emails), 25) # Check call to the connect method to ensure that we authenticate # to the right mail server with the right login self.assertEqual(self.connect_mocked.call_count, 4, 'Must be called once per batch which share the same mail server and the same smtp from') self.connect_mocked.assert_has_calls( calls=[ call(smtp_from='notifications@test.com', mail_server_id=self.server_notification.id), call(smtp_from='user_1@test_2.com', mail_server_id=self.server_domain_2.id), call(smtp_from='user_2@test_2.com', mail_server_id=self.server_domain_2.id), call(smtp_from='user_1@test_2.com', mail_server_id=self.server_domain.id), ], any_order=True, ) self.assert_email_sent_smtp(message_from='"test" ', emails_count=5, from_filter=self.server_notification.from_filter) self.assert_email_sent_smtp(message_from='"test_2" ', emails_count=5, from_filter=self.server_notification.from_filter) self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter) self.assert_email_sent_smtp(message_from='user_2@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter) self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain.from_filter) class TestMailMailRace(common.TransactionCase): @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_bounce_during_send(self): self.partner = self.env['res.partner'].create({ 'name': 'Ernest Partner', }) # we need to simulate a mail sent by the cron task, first create mail, message and notification by hand mail = self.env['mail.mail'].sudo().create({ 'body_html': '

Test

', 'is_notification': True, 'state': 'outgoing', 'recipient_ids': [(4, self.partner.id)] }) message = self.env['mail.message'].create({ 'subject': 'S', 'body': 'B', 'subtype_id': self.ref('mail.mt_comment'), 'notification_ids': [(0, 0, { 'res_partner_id': self.partner.id, 'mail_mail_id': mail.id, 'notification_type': 'email', 'is_read': True, 'notification_status': 'ready', })], }) notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)]) # we need to commit transaction or cr will keep the lock on notif self.cr.commit() # patch send_email in order to create a concurent update and check the notif is already locked by _send() this = self # coding in javascript ruinned my life bounce_deferred = [] @api.model def send_email(self, message, *args, **kwargs): with this.registry.cursor() as cr, mute_logger('odoo.sql_db'): try: # try ro aquire lock (no wait) on notification (should fail) cr.execute("SELECT notification_status FROM mail_notification WHERE id = %s FOR UPDATE NOWAIT", [notif.id]) except psycopg2.OperationalError: # record already locked by send, all good bounce_deferred.append(True) else: # this should trigger psycopg2.extensions.TransactionRollbackError in send(). # Only here to simulate the initial use case # If the record is lock, this line would create a deadlock since we are in the same thread # In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced cr.execute("UPDATE mail_notification SET notification_status='bounce' WHERE id = %s", [notif.id]) return message['Message-Id'] self.env['ir.mail_server']._patch_method('send_email', send_email) mail.send() self.assertTrue(bounce_deferred, "The bounce should have been deferred") self.assertEqual(notif.notification_status, 'sent') # some cleaning since we commited the cr self.env['ir.mail_server']._revert_method('send_email') notif.unlink() message.unlink() mail.unlink() self.partner.unlink() self.env.cr.commit() # because we committed the cursor, the savepoint of the test method is # gone, and this would break TransactionCase cleanups self.cr.execute('SAVEPOINT test_%d' % self._savepoint_id)