# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from unittest.mock import patch from odoo import tools from odoo.addons.base.tests import test_mail_examples from odoo.addons.base.tests.common import MockSmtplibCase from odoo.tests import tagged from odoo.tests.common import TransactionCase from odoo.tools import mute_logger from odoo.tools import config @tagged('mail_server') class TestIrMailServer(TransactionCase, MockSmtplibCase): def setUp(self): self._init_mail_config() self._init_mail_servers() def _build_email(self, mail_from, return_path=None): return self.env['ir.mail_server'].build_email( email_from=mail_from, email_to='dest@example-é.com', subject='subject', body='body', headers={'Return-Path': return_path} if return_path else None ) def test_match_from_filter(self): """Test the from_filter field on the "ir.mail_server".""" match_from_filter = self.env['ir.mail_server']._match_from_filter # Should match tests = [ ('admin@mail.example.com', 'mail.example.com'), ('admin@mail.example.com', 'mail.EXAMPLE.com'), ('admin@mail.example.com', 'admin@mail.example.com'), ('admin@mail.example.com', False), ('"fake@test.com" ', 'mail.example.com'), ('"fake@test.com" ', 'mail.example.com'), ] for email, from_filter in tests: self.assertTrue(match_from_filter(email, from_filter)) # Should not match tests = [ ('admin@mail.example.com', 'test@mail.example.com'), ('admin@mail.example.com', 'test.com'), ('admin@mail.example.com', 'mail.éxample.com'), ('admin@mmail.example.com', 'mail.example.com'), ('admin@mail.example.com', 'mmail.example.com'), ('"admin@mail.example.com" ', 'mail.example.com'), ] for email, from_filter in tests: self.assertFalse(match_from_filter(email, from_filter)) def test_mail_body(self): bodies = [ 'content', '

content

', '

content

', test_mail_examples.MISC_HTML_SOURCE, test_mail_examples.QUOTE_THUNDERBIRD_HTML, ] expected_list = [ 'content', 'content', 'content', "test1\n*test2*\ntest3\ntest4\ntest5\ntest6 test7\ntest8 test9\ntest10\ntest11\ntest12\ngoogle [1]\ntest link [2]\n\n\n[1] http://google.com\n[2] javascript:alert('malicious code')", 'On 01/05/2016 10:24 AM, Raoul\nPoilvache wrote:\n\n* Test reply. The suite. *\n\n--\nRaoul Poilvache\n\nTop cool !!!\n\n--\nRaoul Poilvache', ] for body, expected in zip(bodies, expected_list): message = self.env['ir.mail_server'].build_email( 'john.doe@from.example.com', 'destinataire@to.example.com', body=body, subject='Subject', subtype='html', ) body_alternative = False for part in message.walk(): if part.get_content_maintype() == 'multipart': continue # skip container if part.get_content_type() == 'text/plain': if not part.get_payload(): continue body_alternative = tools.ustr(part.get_content()) # remove ending new lines as it just adds noise body_alternative = body_alternative.strip('\n') self.assertEqual(body_alternative, expected) @mute_logger('odoo.models.unlink') def test_mail_server_priorities(self): """Test if we choose the right mail server to send an email. Priorities are 1. Forced mail server (e.g.: in mass mailing) - If the "from_filter" of the mail server match the notification email use the notifications email in the "From header" - Otherwise spoof the "From" (because we force the mail server but we don't know which email use to send it) 2. A mail server for which the "from_filter" match the "From" header 3. A mail server for which the "from_filter" match the domain of the "From" header 4. The mail server used for notifications 5. A mail server without "from_filter" (and so spoof the "From" header because we do not know for which email address it can be used) """ # sanity checks self.assertTrue(self.env['ir.mail_server']._get_default_from_address(), 'Notifications email must be set for testing') self.assertTrue(self.env['ir.mail_server']._get_default_bounce_address(), 'Bounce email must be set for testing') mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='specific_user@test.com') self.assertEqual(mail_server, self.server_user) self.assertEqual(mail_from, 'specific_user@test.com') mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Name name@strange.name" ') self.assertEqual(mail_server, self.server_user, 'Must extract email from full name') self.assertEqual(mail_from, '"Name name@strange.name" ', 'Must keep the given mail from') # Should not be case sensitive mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='specific_user@test.com') self.assertEqual(mail_server, self.server_user, 'Mail from is case insensitive') self.assertEqual(mail_from, 'specific_user@test.com', 'Should not change the mail from') mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='unknown_email@test.com') self.assertEqual(mail_server, self.server_domain) self.assertEqual(mail_from, 'unknown_email@test.com') # Cover a different condition that the "email case insensitive" test mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='unknown_email@TEST.COM') self.assertEqual(mail_server, self.server_domain, 'Domain is case insensitive') self.assertEqual(mail_from, 'unknown_email@TEST.COM', 'Domain is case insensitive') mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='"Test" ') self.assertEqual(mail_server, self.server_notification, 'Should take the notification email') self.assertEqual(mail_from, 'notifications@test.com') # test if notification server is selected if email_from = False mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from=False) self.assertEqual(mail_server, self.server_notification, 'Should select the notification email server if passed FROM address was False') self.assertEqual(mail_from, 'notifications@test.com') # remove the notifications email to simulate a mis-configured Odoo database # so we do not have the choice, we have to spoof the FROM # (otherwise we can not send the email) self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', False) with mute_logger('odoo.addons.base.models.ir_mail_server'): mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from='test@unknown_domain.com') self.assertEqual(mail_server.from_filter, False, 'No notifications email set, must be forced to spoof the FROM') self.assertEqual(mail_from, 'test@unknown_domain.com') @mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_mail_server') def test_mail_server_send_email(self): IrMailServer = self.env['ir.mail_server'] default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() # A mail server is configured for the email with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message) self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( smtp_from='specific_user@test.com', message_from='specific_user@test.com', from_filter='specific_user@test.com', ) # No mail server are configured for the email address, # so it will use the notifications email instead and encapsulate the old email with self.mock_smtplib_connection(): message = self._build_email(mail_from='"Name" ') IrMailServer.send_email(message) self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( smtp_from='notifications@test.com', message_from='"Name" ', from_filter='notifications@test.com', ) # Same situation, but the original email has no name part with self.mock_smtplib_connection(): message = self._build_email(mail_from='test@unknown_domain.com') IrMailServer.send_email(message) self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( smtp_from='notifications@test.com', message_from='"test" ', from_filter='notifications@test.com', ) # A mail server is configured for the entire domain name, so we can use the bounce # email address because the mail server supports it with self.mock_smtplib_connection(): message = self._build_email(mail_from='unknown_name@test.com') IrMailServer.send_email(message) self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='unknown_name@test.com', from_filter='test.com', ) # remove the notification server # so will use the mail server self.server_notification.unlink() # The mail server configured for the notifications email has been removed # but we can still use the mail server configured for test.com # and so we will be able to use the bounce address # because we use the mail server for "test.com" with self.mock_smtplib_connection(): message = self._build_email(mail_from='"Name" ') IrMailServer.send_email(message) self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='"Name" ', from_filter='test.com', ) # Test that the mail from / recipient envelop are encoded using IDNA self.server_domain.from_filter = 'ééééééé.com' self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'ééééééé.com') with self.mock_smtplib_connection(): message = self._build_email(mail_from='test@ééééééé.com') IrMailServer.send_email(message) self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( smtp_from='bounce.test@xn--9caaaaaaa.com', smtp_to_list=['dest@xn--example--i1a.com'], message_from='test@=?utf-8?b?w6nDqcOpw6nDqcOpw6k=?=.com', from_filter='ééééééé.com', ) # Test the case when the "mail.default.from" contains a full email address and not just the local part # the domain of this default email address can be different than the catchall domain self.env['ir.config_parameter'].sudo().set_param('mail.default.from', 'test@custom_domain.com') self.server_default.from_filter = 'custom_domain.com' with self.mock_smtplib_connection(): message = self._build_email(mail_from='"Name" ') IrMailServer.send_email(message) self.assert_email_sent_smtp( smtp_from='test@custom_domain.com', smtp_to_list=['dest@xn--example--i1a.com'], message_from='"Name" ', from_filter='custom_domain.com', ) # Test when forcing the mail server and when smtp_encryption is "starttls" self.server_domain.smtp_encryption = "starttls" self.server_domain.from_filter = "test.com" with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message, mail_server_id=self.server_domain.id) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from='specific_user@test.com', message_from='specific_user@test.com', from_filter='test.com', ) # miss-configured database, no mail servers from filter # match the user / notification email self.env['ir.mail_server'].search([]).from_filter = "random.domain" with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from='test@custom_domain.com', message_from='"specific_user" ', from_filter='random.domain', ) @mute_logger('odoo.models.unlink') def test_mail_server_send_email_smtp_session(self): """Test all the cases when we provide the SMTP session. The results must be the same as passing directly the parameter to "send_email". """ IrMailServer = self.env['ir.mail_server'] default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() # A mail server is configured for the email with self.mock_smtplib_connection(): smtp_session = IrMailServer.connect(smtp_from='specific_user@test.com') message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message, smtp_session=smtp_session) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from='specific_user@test.com', message_from='specific_user@test.com', from_filter='specific_user@test.com', ) # No mail server are configured for the email address, # so it will use the notifications email instead and encapsulate the old email with self.mock_smtplib_connection(): smtp_session = IrMailServer.connect(smtp_from='"Name" ') message = self._build_email(mail_from='"Name" ') IrMailServer.send_email(message, smtp_session=smtp_session) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from='notifications@test.com', message_from='"Name" ', from_filter='notifications@test.com', ) # A mail server is configured for the entire domain name, so we can use the bounce # email address because the mail server supports it with self.mock_smtplib_connection(): smtp_session = IrMailServer.connect(smtp_from='unknown_name@test.com') message = self._build_email(mail_from='unknown_name@test.com') IrMailServer.send_email(message, smtp_session=smtp_session) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='unknown_name@test.com', from_filter='test.com', ) # remove the notification server # so will use the mail server self.server_notification.unlink() # The mail server configured for the notifications email has been removed # but we can still use the mail server configured for test.com with self.mock_smtplib_connection(): smtp_session = IrMailServer.connect(smtp_from='"Name" ') message = self._build_email(mail_from='"Name" ') IrMailServer.send_email(message, smtp_session=smtp_session) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='"Name" ', from_filter='test.com', ) @mute_logger('odoo.models.unlink') @patch.dict(config.options, {"from_filter": "test.com", "smtp_server": "example.com"}) def test_mail_server_binary_arguments_domain(self): """Test the configuration provided in the odoo-bin arguments. This config is used when no mail server exists. """ IrMailServer = self.env['ir.mail_server'] default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() # Remove all mail server so we will use the odoo-bin arguments self.env['ir.mail_server'].search([]).unlink() self.assertFalse(self.env['ir.mail_server'].search([])) # Use an email in the domain of the "from_filter" with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='specific_user@test.com', from_filter='test.com', ) # Test if the domain name is normalized before comparison with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='specific_user@test.com', from_filter='test.com', ) # Use an email outside of the domain of the "from_filter" # So we will use the notifications email in the headers and the bounce address # in the envelop because the "from_filter" allows to use the entire domain with self.mock_smtplib_connection(): message = self._build_email(mail_from='test@unknown_domain.com') IrMailServer.send_email(message) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='"test" ', from_filter='test.com', ) @mute_logger('odoo.models.unlink') @patch.dict(config.options, {"from_filter": "test.com", "smtp_server": "example.com"}) def test_mail_server_binary_arguments_domain_smtp_session(self): """Test the configuration provided in the odoo-bin arguments. This config is used when no mail server exists. Use a pre-configured SMTP session. """ IrMailServer = self.env['ir.mail_server'] default_bounce_adress = self.env['ir.mail_server']._get_default_bounce_address() # Remove all mail server so we will use the odoo-bin arguments self.env['ir.mail_server'].search([]).unlink() self.assertFalse(self.env['ir.mail_server'].search([])) # Use an email in the domain of the "from_filter" with self.mock_smtplib_connection(): smtp_session = IrMailServer.connect(smtp_from='specific_user@test.com') message = self._build_email(mail_from='specific_user@test.com') IrMailServer.send_email(message, smtp_session=smtp_session) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='specific_user@test.com', from_filter='test.com', ) # Use an email outside of the domain of the "from_filter" # So we will use the notifications email in the headers and the bounce address # in the envelop because the "from_filter" allows to use the entire domain with self.mock_smtplib_connection(): smtp_session = IrMailServer.connect(smtp_from='test@unknown_domain.com') message = self._build_email(mail_from='test@unknown_domain.com') IrMailServer.send_email(message, smtp_session=smtp_session) self.connect_mocked.assert_called_once() self.assert_email_sent_smtp( smtp_from=default_bounce_adress, message_from='"test" ', from_filter='test.com', ) def test_mail_server_get_email_addresses(self): """Test the email used to test the mail server connection.""" self.server_notification.from_filter = 'example_2.com' self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') email_from = self.server_notification._get_test_email_addresses()[0] self.assertEqual(email_from, 'noreply@example_2.com') self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications') email_from = self.server_notification._get_test_email_addresses()[0] self.assertEqual(email_from, 'notifications@example_2.com') self.server_notification.from_filter = 'full_email@example_2.com' self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications') email_from = self.server_notification._get_test_email_addresses()[0] self.assertEqual(email_from, 'full_email@example_2.com') self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') email_from = self.server_notification._get_test_email_addresses()[0] self.assertEqual(email_from, 'full_email@example_2.com') self.env['ir.config_parameter'].set_param('mail.default.from', 'notifications@example.com') self.server_notification.from_filter = 'example.com' email_from = self.server_notification._get_test_email_addresses()[0] self.assertEqual(email_from, 'notifications@example.com') @mute_logger('odoo.models.unlink') @patch.dict(config.options, {'from_filter': 'test.com', 'smtp_server': 'example.com'}) def test_mail_server_mail_default_from_filter(self): """Test that the config parameter "mail.default.from_filter" overwrite the odoo-bin argument "--from-filter" """ self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', 'example.com') IrMailServer = self.env['ir.mail_server'] # Remove all mail server so we will use the odoo-bin arguments IrMailServer.search([]).unlink() self.assertFalse(IrMailServer.search([])) # Use an email in the domain of the config parameter "mail.default.from_filter" with self.mock_smtplib_connection(): message = self._build_email(mail_from='specific_user@example.com') IrMailServer.send_email(message) self.assert_email_sent_smtp( smtp_from='specific_user@example.com', message_from='specific_user@example.com', from_filter='example.com', ) def test_eml_attachment_encoding(self): """Test that message/rfc822 attachments are encoded using 7bit, 8bit, or binary encoding.""" IrMailServer = self.env['ir.mail_server'] # Create a sample .eml file content eml_content = b"From: user@example.com\nTo: user2@example.com\nSubject: Test Email\n\nThis is a test email." attachments = [('test.eml', eml_content, 'message/rfc822')] # Build the email with the .eml attachment message = IrMailServer.build_email( email_from='john.doe@from.example.com', email_to='destinataire@to.example.com', subject='Subject with .eml attachment', body='This email contains a .eml attachment.', attachments=attachments, ) # Verify that the attachment is correctly encoded acceptable_encodings = {'7bit', '8bit', 'binary'} for part in message.iter_attachments(): if part.get_content_type() == 'message/rfc822': self.assertIn( part.get('Content-Transfer-Encoding'), acceptable_encodings, "The message/rfc822 attachment should be encoded using 7bit, 8bit, or binary encoding.", )