# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import json import uuid from email.message import EmailMessage from unittest.mock import patch import trytond.config as config from trytond.modules.inbound_email.ir import ( REPLY_LINE, _get_channel_identifier) from trytond.pool import Pool from trytond.protocols.wrappers import HTTPStatus from trytond.tests.test_tryton import ( ModuleTestCase, RouteTestCase, with_transaction) from trytond.transaction import Transaction class InboundEmailTestCase(ModuleTestCase): "Test Inbound Email module" module = 'inbound_email' @classmethod def setUpClass(cls): super().setUpClass() email_from = config.get('email', 'from', default='') config.set('email', 'from', 'tryton@example.org') cls.addClassCleanup(config.set, 'email', 'from', email_from) config.add_section('inbound_email') config.set('inbound_email', 'chat_reply_to', 'chat@example.org') cls.addClassCleanup(config.remove_section, 'inbound_email') def get_message(self): message = EmailMessage() message['From'] = "John Doe " message['To'] = ( "Michael Scott , pam@example.com") message['Cc'] = 'jim@example.com' message['Subject'] = "The office" message.set_content("Hello") message.add_attachment( b'bin', maintype='application', subtype='octet-stream', filename='data.bin') return message def get_message_dict(self, headers): return { 'from': 'john.doe@example.com', 'to': ['michael@example.com', 'pam@example.com'], 'cc': ['jim@example.com'], 'subject': 'The office', 'text': 'Hello\n', 'attachments': [{ 'filename': 'data.bin', 'type': 'application/octet-stream', 'data': b'bin', }], 'headers': dict(headers.items()), } @with_transaction() def test_inbox_identifier(self): "Test inbox identifier" pool = Pool() Inbox = pool.get('inbound.email.inbox') inbox = Inbox(name="Test") inbox.save() self.assertFalse(inbox.identifier) self.assertFalse(inbox.endpoint) Inbox.new_identifier([inbox]) self.assertTrue(inbox.identifier) self.assertTrue(inbox.endpoint) Inbox.new_identifier([inbox]) self.assertFalse(inbox.identifier) self.assertFalse(inbox.endpoint) @with_transaction() def test_email_from_webhook(self): "Test email from webhook" pool = Pool() Email = pool.get('inbound.email') Inbox = pool.get('inbound.email.inbox') message = self.get_message() inbox = Inbox() data = message.as_bytes() for data_type in ['raw', 'mailpace', 'postmark', 'sendgrid']: with self.subTest(data_type=data_type): emails = Email.from_webhook(inbox, data, data_type=data_type) email, = emails self.assertEqual(email.inbox, inbox) self.assertEqual(email.data, data) self.assertEqual(email.data_type, data_type) with self.subTest(data_type='mailchimp'): data = json.dumps({ 'mandrill_events': [{ 'event': 'inbound', }, { 'event': 'inbound', }], }) emails = Email.from_webhook(inbox, data, data_type='mailchimp') self.assertEqual(len(emails), 2) email = emails[0] self.assertEqual(email.inbox, inbox) self.assertEqual(json.loads(email.data), {'event': 'inbound'}) self.assertEqual(email.data_type, 'mailchimp') @with_transaction() def test_email_as_dict_raw(self): "Test raw email as dict" pool = Pool() Email = pool.get('inbound.email') message = self.get_message() email = Email(data=message.as_bytes(), data_type='raw') self.assertDictEqual(email.as_dict(), self.get_message_dict(message)) @with_transaction() def test_email_as_dict_mailchimp(self): "Test mailchimp email as dict" pool = Pool() Email = pool.get('inbound.email') message = self.get_message() data = json.dumps({ 'event': 'inbound', 'raw_msg': message.as_string(), }).encode('utf-8') email = Email(data=data, data_type='mailchimp') self.assertDictEqual(email.as_dict(), self.get_message_dict(message)) @with_transaction() def test_email_as_dict_mailpace(self): "Test mailpace email as dict" pool = Pool() Email = pool.get('inbound.email') message = self.get_message() data = json.dumps({ 'raw': message.as_string(), }).encode('utf-8') email = Email(data=data, data_type='mailpace') self.assertDictEqual(email.as_dict(), self.get_message_dict(message)) @with_transaction() def test_email_as_dict_postmark(self): "Test postmark email as dict" pool = Pool() Email = pool.get('inbound.email') data = json.dumps({ 'FromFull': { 'Name': 'John Doe', 'Email': 'john.doe@example.com', }, 'ToFull': [{ 'Name': 'Michael', 'Email': 'michael@example.com', }, { 'Email': 'pam@example.com', }, ], 'CcFull': [{ 'Email': 'jim@example.com', }], 'Subject': 'The office', 'TextBody': 'Hello\n', 'Attachments': [{ 'Name': 'data.bin', 'Content': 'Ymlu', 'ContentType': 'application/octet-stream', }], 'Headers': [{ 'Name': 'Message-ID', 'Value': '12345@example.com', }], }).encode('utf-8') email = Email(data=data, data_type='postmark') self.assertDictEqual( email.as_dict(), self.get_message_dict({'Message-ID': '12345@example.com'})) @with_transaction() def test_email_as_dict_sendgrid(self): "Test sendgrid email as dict" pool = Pool() Email = pool.get('inbound.email') message = self.get_message() data = json.dumps({ 'email': message.as_string(), }).encode('utf-8') email = Email(data=data, data_type='sendgrid') self.assertDictEqual(email.as_dict(), self.get_message_dict(message)) @with_transaction() def test_email_process(self): "Test email process" pool = Pool() Inbox = pool.get('inbound.email.inbox') Rule = pool.get('inbound.email.rule') Email = pool.get('inbound.email') inbox = Inbox(name="Test") inbox.rules = [Rule()] inbox.save() message = self.get_message() email = Email(inbox=inbox, data=message.as_bytes(), data_type='raw') email.save() with patch.object(Rule, 'run') as run: Email.process([email]) run.assert_called_once_with(email) def _get_rule(self, **values): pool = Pool() Rule = pool.get('inbound.email.rule') for name in Rule._fields: values.setdefault(name, None) return Rule(**values) @with_transaction() def test_rule_match_origin(self): "Test rule match origin" rule = self._get_rule(origin="foo.*@example.com") self.assertTrue(rule.match({'from': "foo@example.com"})) self.assertFalse(rule.match({'from': "bar@example.com"})) self.assertFalse(rule.match({})) @with_transaction() def test_rule_match_destination(self): "Test rule match destination" rule = self._get_rule(destination="foo.*@example.com") self.assertTrue(rule.match({'to': ["foo@example.com"]})) self.assertTrue(rule.match({'to': [ "bar@example.com", "foo@example.com"]})) self.assertTrue(rule.match({'cc': ["foo@example.com"]})) self.assertTrue(rule.match({'bcc': ["foo@example.com"]})) self.assertFalse(rule.match({'to': ["bar@example.com"]})) self.assertFalse(rule.match({})) @with_transaction() def test_rule_match_subject(self): "Test rule match subject" rule = self._get_rule(subject="Test") self.assertTrue(rule.match({'subject': "Email test subject"})) self.assertFalse(rule.match({'subject': "Email subject"})) self.assertFalse(rule.match({})) @with_transaction() def test_rule_match_attachment_name(self): "Test rule match attachment name" rule = self._get_rule(attachment_name="foo") self.assertTrue(rule.match({'attachments': [ {'filename': "foo.pdf"}]})) self.assertTrue(rule.match({'attachments': [ {'filename': "bar.pdf"}, {'filename': "foo.pdf"}]})) self.assertFalse(rule.match({'attachments': [ {'filename': "bar.pdf"}]})) self.assertFalse(rule.match({})) @with_transaction() def test_rule_match_headers(self): "Test rule match headers" rule = self._get_rule( headers=[{'name': 'Message-ID', 'value': 'tryton-'}]) self.assertTrue(rule.match({'headers': {'Message-ID': 'tryton-42'}})) self.assertTrue(rule.match({'headers': { 'From': 'foo@example.com', 'Message-ID': 'tryton-42', }})) self.assertFalse(rule.match({'headers': {'Message-ID': 'bar-42'}})) self.assertFalse(rule.match({'headers': { 'From': 'tryton-test@example.com', 'Message-ID': 'bar-42', }})) self.assertFalse(rule.match({})) @with_transaction() def test_rule_run(self): "Test run rule" pool = Pool() Rule = pool.get('inbound.email.rule') Email = pool.get('inbound.email') rule = Rule(action='inbound.email|test') email = Email() with patch.object(Email, 'test', create=True) as func: func.return_value = result = Rule() rule.run(email) func.assert_called_once_with(email, rule) self.assertEqual(email.result, result) def test_get_channel_identifier(self): "Test parsing the recipients to find the email channel" channel_id = uuid.uuid4().hex for value, expected in [ (f'chat+{channel_id}@example.org', channel_id), (['chat@example.org', f'chat+{channel_id}@example.org'], channel_id), ('chat@example.org', None), ('chat+foo@example.org', None), (f'{channel_id}@example.org', None), (f'chat+{channel_id}@example.com', None), ]: for recipient_hdr in ('To', 'Cc'): with self.subTest(f"{recipient_hdr}: {value}"): msg = EmailMessage() msg[recipient_hdr] = value self.assertEqual(_get_channel_identifier(msg), expected) @with_transaction() def test_find_email_content(self): "Test finding the content of the reply" pool = Pool() Channel = pool.get('ir.chat.channel') for (body, expected) in [ ("Foo", "Foo"), ("Foo\n-----\nBar", "Foo\n-----\nBar"), (f"Foo\n{REPLY_LINE}\nLorem Ipsum", "Foo"), (f"Foo\n> {REPLY_LINE}\n> Lorem Ipsum", "Foo"), ]: with self.subTest(body): self.assertEqual(Channel._email_content(body), expected) class InboundEmailRouteTestCase(RouteTestCase): "Test Inbound Email route" module = 'inbound_email' identifier = uuid.uuid4().hex @classmethod def setUpDatabase(cls): pool = Pool() Inbox = pool.get('inbound.email.inbox') Inbox(name="Test", identifier=cls.identifier).save() def test_inbound_email_route_data(self): "Test inbound email route with data" client = self.client() response = client.post( f'/{self.db_name}/inbound_email/inbox/{self.identifier}', data=b'data') self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) @with_transaction() def check(): pool = Pool() Email = pool.get('inbound.email') email, = Email.search([]) try: self.assertEqual(email.data, b'data') self.assertEqual(email.data_type, 'raw') self.assertTrue(email.inbox) finally: Email.delete([email]) Transaction().commit() check() def test_inbound_email_route_form(self): "Test inbound email route with form" client = self.client() response = client.post( f'/{self.db_name}/inbound_email/inbox/{self.identifier}', data={ 'email': 'data', }) self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) @with_transaction() def check(): pool = Pool() Email = pool.get('inbound.email') email, = Email.search([]) try: self.assertEqual(email.data, b'{"email": "data"}') self.assertEqual(email.data_type, 'raw') self.assertTrue(email.inbox) finally: Email.delete([email]) Transaction().commit() check() def test_inbound_email_route_not_found(self): "Test inbound email route not found" client = self.client() response = client.post( f'/{self.db_name}/inbound_email/inbox/unknown', data=b'foo') self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) del ModuleTestCase, RouteTestCase