diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index 69bc85fc326..b5d609d09ba 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -111,6 +111,7 @@ class MockEmail(common.BaseCase, MockSmtplibCase): 'smtp_host': 'smtp_host', } ]) + cls.mail_servers = cls.mail_server_domain + cls.mail_server_global # ------------------------------------------------------------ # GATEWAY TOOLS @@ -507,6 +508,7 @@ class MockEmail(common.BaseCase, MockSmtplibCase): debug_info = '-'.join('From: %s-To: %s' % (mail['email_from'], mail['email_to']) for mail in self._mails) if not bool(sent_mail) else '' self.assertTrue(bool(sent_mail), 'Expected mail from %s to %s not found in %s' % (expected['email_from'], expected['email_to'], debug_info)) + # assert values for val in direct_check: if val in expected: self.assertEqual(expected[val], sent_mail[val], 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val])) diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py index a992e039f7b..8a10c94fbb5 100644 --- a/addons/test_mail/tests/test_mail_mail.py +++ b/addons/test_mail/tests/test_mail_mail.py @@ -62,7 +62,7 @@ class TestMailMail(TestMailCommon): def _reset_data(self): self._init_mail_mock() self.test_mail.write({'failure_reason': False, 'failure_type': False, 'state': 'outgoing'}) - self.test_notification.write({'failure_type': False, 'notification_status': 'ready'}) + self.test_notification.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'}) @users('admin') def test_mail_mail_attachment_access(self): @@ -150,6 +150,46 @@ class TestMailMail(TestMailCommon): self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted]) self.assertEqual(len(self._mails), 2) + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_recipients_cc(self): + """ Partner_ids is a field used from mail_message, but not from mail_mail. """ + mail = self.env['mail.mail'].sudo().create({ + 'body_html': '

Test

', + 'email_cc': 'test.cc.1@example.com, "Herbert" ', + 'email_to': 'test.rec.1@example.com, "Raoul" ', + 'recipient_ids': [(4, self.user_employee.partner_id.id)], + }) + + with self.mock_mail_gateway(): + mail.send() + # note that formatting is lost for cc + self.assertSentEmail(mail.env.user.partner_id, + ['test.rec.1@example.com', '"Raoul" '], + email_cc=['test.cc.1@example.com', 'test.cc.2@example.com']) + # Mail: currently cc are put as copy of all sent emails (aka spam) + self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted], + email_cc=['test.cc.1@example.com', 'test.cc.2@example.com']) + self.assertEqual(len(self._mails), 2) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_recipients_formatting(self): + """ Check support of email / formatted email """ + mail = self.env['mail.mail'].sudo().create({ + 'author_id': False, + 'body_html': '

Test

', + 'email_cc': 'test.cc.1@example.com, "Herbert" ', + 'email_from': '"Ignasse" ', + 'email_to': 'test.rec.1@example.com, "Raoul" ', + }) + + with self.mock_mail_gateway(): + mail.send() + # note that formatting is lost for cc + self.assertSentEmail('"Ignasse" ', + ['test.rec.1@example.com', '"Raoul" '], + email_cc=['test.cc.1@example.com', 'test.cc.2@example.com']) + self.assertEqual(len(self._mails), 1) + @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_mail_return_path(self): # mail without thread-enabled record @@ -235,11 +275,14 @@ class TestMailMail(TestMailCommon): with self.mock_mail_gateway(), mute_logger('odoo.addons.mail.models.mail_mail'): mail.send(raise_exception=False) self.assertFalse(self._mails[0]['email_from']) - self.assertIn( - 'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs', - mail.failure_reason) + self.assertEqual( + mail.failure_reason, + 'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs, in the server configuration file or with the --email-from startup parameter.') self.assertFalse(mail.failure_type, 'Mail: void from: no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertEqual( + notification.failure_reason, + 'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs, in the server configuration file or with the --email-from startup parameter.') self.assertEqual(notification.failure_type, 'unknown', 'Mail: void from: unknown failure type, should be updated') self.assertEqual(notification.notification_status, 'exception') @@ -254,6 +297,7 @@ class TestMailMail(TestMailCommon): self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertIn('Codepoint U+00A2 at position', notification.failure_reason) self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated') self.assertEqual(notification.notification_status, 'exception') @@ -267,6 +311,7 @@ class TestMailMail(TestMailCommon): self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) self.assertFalse(mail.failure_type, 'Mail: bugged catchall domain (ascii): no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertIn('Codepoint U+00A2 at position', notification.failure_reason) self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged catchall domain (ascii): unknown failure type, should be updated') self.assertEqual(notification.notification_status, 'exception') @@ -277,9 +322,10 @@ class TestMailMail(TestMailCommon): with self.mock_mail_gateway(): mail.send(raise_exception=False) self.assertEqual(self._mails[0]['email_from'], 'robert') - self.assertIn("Malformed 'Return-Path' or 'From' address: robert", mail.failure_reason) + self.assertEqual(mail.failure_reason, "Malformed 'Return-Path' or 'From' address: robert - It should contain one valid plain ASCII email") self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: robert - It should contain one valid plain ASCII email") self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated') self.assertEqual(notification.notification_status, 'exception') @@ -298,13 +344,15 @@ class TestMailMail(TestMailCommon): mail.write({'email_to': email_to}) with self.mock_mail_gateway(): mail.send(raise_exception=False) - self.assertIn('Error without exception. Probably due to sending an email without computed recipients.', mail.failure_reason) + self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.') self.assertFalse(mail.failure_type, 'Mail: missing email_to: no failure type, should be updated') self.assertEqual(mail.state, 'exception') if email_to == ' ': + self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated') self.assertEqual(notification.failure_type, 'mail_email_missing') self.assertEqual(notification.notification_status, 'exception') else: + self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated') self.assertEqual(notification.failure_type, False, 'Mail: missing email_to: notification is wrongly set as sent') self.assertEqual(notification.notification_status, 'sent', 'Mail: missing email_to: notification is wrongly set as sent') @@ -315,9 +363,10 @@ class TestMailMail(TestMailCommon): mail.write({'email_to': email_to}) with self.mock_mail_gateway(): mail.send(raise_exception=False) - self.assertIn('Error without exception. Probably due to sending an email without computed recipients.', mail.failure_reason) + self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.') self.assertFalse(mail.failure_type, 'Mail: invalid email_to: no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated') self.assertEqual(notification.failure_type, failure_type, 'Mail: invalid email_to: missing instead of invalid') self.assertEqual(notification.notification_status, 'exception') @@ -330,6 +379,7 @@ class TestMailMail(TestMailCommon): self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertIn('Codepoint U+00A2 at position', notification.failure_reason) self.assertEqual(notification.failure_type, 'unknown', 'Mail: invalid (ascii) recipient partner: unknown failure type, should be updated') self.assertEqual(notification.notification_status, 'exception') @@ -342,6 +392,7 @@ class TestMailMail(TestMailCommon): self.assertFalse(mail.failure_reason) self.assertFalse(mail.failure_type) self.assertEqual(mail.state, 'sent') + self.assertFalse(notification.failure_reason) self.assertFalse(notification.failure_type) self.assertEqual(notification.notification_status, 'sent') @@ -379,6 +430,7 @@ class TestMailMail(TestMailCommon): self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.') self.assertFalse(mail.failure_type, 'Mail: void recipient partner: no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated') self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void recipient partner: should be missing, not invalid') self.assertEqual(notification.notification_status, 'exception') @@ -392,6 +444,7 @@ class TestMailMail(TestMailCommon): self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.') self.assertFalse(mail.failure_type, 'Mail: invalid recipient partner: no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated') self.assertEqual(notification.failure_type, 'mail_email_invalid') self.assertEqual(notification.notification_status, 'exception') @@ -405,6 +458,7 @@ class TestMailMail(TestMailCommon): self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated') self.assertEqual(mail.state, 'exception') + self.assertIn('Codepoint U+00A2 at position', notification.failure_reason) self.assertEqual(notification.failure_type, 'unknown', 'Mail: invalid (ascii) recipient partner: unknown failure type, should be updated') self.assertEqual(notification.notification_status, 'exception') @@ -418,6 +472,7 @@ class TestMailMail(TestMailCommon): self.assertFalse(mail.failure_reason) self.assertFalse(mail.failure_type) self.assertEqual(mail.state, 'sent') + self.assertFalse(notification.failure_reason) self.assertFalse(notification.failure_type) self.assertEqual(notification.notification_status, 'sent') @@ -451,6 +506,7 @@ class TestMailMail(TestMailCommon): self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertFalse(notification.failure_reason, 'Mail: void email considered as invalid') self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void email considered as invalid') self.assertEqual(notification.notification_status, 'exception') @@ -468,30 +524,34 @@ class TestMailMail(TestMailCommon): # missing to / invalid to for email_to in self.emails_falsy + self.emails_invalid: self._reset_data() - notification2.write({'failure_type': False, 'notification_status': 'ready'}) + notification2.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'}) mail.write({'email_to': email_to}) with self.mock_mail_gateway(): mail.send(raise_exception=False) self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertFalse(notification.failure_reason) self.assertFalse(notification.failure_type) self.assertEqual(notification.notification_status, 'sent') + self.assertFalse(notification2.failure_reason) self.assertEqual(notification2.failure_type, 'mail_email_invalid') self.assertEqual(notification2.notification_status, 'exception') # buggy to (ascii) for email_to in self.emails_invalid_ascii: self._reset_data() - notification2.write({'failure_type': False, 'notification_status': 'ready'}) + notification2.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'}) mail.write({'email_to': email_to}) with self.mock_mail_gateway(): mail.send(raise_exception=False) self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') self.assertEqual(mail.state, 'exception') + self.assertIn('Codepoint U+00A2 at position', notification.failure_reason) self.assertEqual(notification.failure_type, 'unknown') self.assertEqual(notification.notification_status, 'exception') + self.assertIn('Codepoint U+00A2 at position', notification2.failure_reason) self.assertEqual(notification2.failure_type, 'unknown') self.assertEqual(notification2.notification_status, 'exception') @@ -521,9 +581,10 @@ class TestMailMail(TestMailCommon): self.connect_mocked.side_effect = _connect mail.send(raise_exception=False) - self.assertFalse(mail.failure_type) self.assertEqual(mail.failure_reason, msg) + self.assertFalse(mail.failure_type) self.assertEqual(mail.state, 'exception') + self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated') self.assertEqual(notification.failure_type, 'mail_smtp') self.assertEqual(notification.notification_status, 'exception') self._reset_data() @@ -549,6 +610,7 @@ class TestMailMail(TestMailCommon): self.assertFalse(mail.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') self.assertFalse(mail.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') self.assertEqual(mail.state, 'outgoing', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') + self.assertFalse(notification.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') self.assertFalse(notification.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') self.assertEqual(notification.notification_status, 'ready', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') @@ -566,6 +628,7 @@ class TestMailMail(TestMailCommon): self.assertEqual(mail.failure_reason, msg) self.assertFalse(mail.failure_type, 'Mail: unlogged failure type to fix') self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_reason, msg) self.assertEqual(notification.failure_type, 'unknown', 'Mail: generic failure type') self.assertEqual(notification.notification_status, 'exception') diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 1c29eb29943..a47adb8e1f3 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -698,26 +698,68 @@ class TestMailComplexPerformance(BaseMailPerformance): @warmup def test_complex_mail_mail_send(self): message = self.env['mail.message'].sudo().create({ - 'subject': 'Test', - 'body': '

Test

', 'author_id': self.env.user.partner_id.id, + 'body': '

Test

', 'email_from': self.env.user.partner_id.email, + 'message_type': 'comment', 'model': 'mail.test.container', 'res_id': self.container.id, + 'subject': 'Test', }) + attachments = self.env['ir.attachment'].create([ + dict(attachment, res_id=self.container.id, res_model='mail.test.container') + for attachment in self.test_attachments_vals + ]) mail = self.env['mail.mail'].sudo().create({ + 'attachment_ids': [(4, att.id) for att in attachments], + 'auto_delete': False, 'body_html': '

Test

', 'mail_message_id': message.id, 'recipient_ids': [(4, pid) for pid in self.partners.ids], }) - mail_ids = mail.ids with self.assertQueryCount(__system__=9, employee=9): - self.env['mail.mail'].sudo().browse(mail_ids).send() + self.env['mail.mail'].sudo().browse(mail.ids).send() - self.assertEqual(mail.body_html, '

Test

') - self.assertEqual(mail.reply_to, formataddr( - ('%s %s' % (self.env.company.name, self.container.name), 'test-alias@%s' % self.alias_domain) - )) + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'employee') + @warmup + def test_complex_mail_mail_send_batch_complete(self): + """ A more complete use case: 10 mails, attachments, servers, ... And + 2 failing emails. """ + message = self.env['mail.message'].sudo().create({ + 'author_id': self.env.user.partner_id.id, + 'email_from': self.env.user.partner_id.email, + 'message_type': 'comment', + 'model': 'mail.test.container', + 'res_id': self.container.id, + 'subject': 'Test', + }) + attachments = self.env['ir.attachment'].create([ + dict(attachment, res_id=self.container.id, res_model='mail.test.container') + for attachment in self.test_attachments_vals + ]) + mails = self.env['mail.mail'].sudo().create([{ + 'attachment_ids': [(4, att.id) for att in attachments], + 'auto_delete': True, + 'body_html': '

Test %s

' % idx, + 'email_cc': 'cc.1@test.example.com, cc.2@test.example.com', + 'email_to': 'customer.1@example.com, customer.2@example.com', + 'mail_message_id': message.id, + 'mail_server_id': self.mail_servers.ids[idx % len(self.mail_servers.ids)], + 'recipient_ids': [(4, pid) for pid in self.partners.ids], + } for idx in range(12)]) + mails[-2].write({'email_cc': False, 'email_to': 'strange@example¢¡.com'}) + mails[-1].write({'email_cc': False, 'email_to': 'void', 'recipient_ids': [(5, 0)]}) + with self.assertQueryCount(__system__=44, employee=44): + self.env['mail.mail'].sudo().browse(mails.ids).send() + + for mail in mails[:-2]: + self.assertEqual(mail.state, 'sent') + self.assertTrue(mail.to_delete, 'Mail: sent mails are to be unlinked') + self.assertEqual(mails[-2].state, 'exception') + self.assertFalse(mails[-2].to_delete, 'Mail: invalid recipient, not to unlink') + self.assertEqual(mails[-1].state, 'exception') + self.assertFalse(mails[-2].to_delete, 'Mail: ninvalid recipient, not to unlink') @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') @users('__system__', 'employee')