diff --git a/zerver/management/commands/send_to_email_mirror.py b/zerver/management/commands/send_to_email_mirror.py new file mode 100644 index 0000000000..17cc38126d --- /dev/null +++ b/zerver/management/commands/send_to_email_mirror.py @@ -0,0 +1,117 @@ + +import os +import email +import ujson + +from email.message import Message +from email.mime.text import MIMEText + +from django.conf import settings +from django.core.management.base import CommandParser +from django.core.mail import send_mail + +from zerver.lib.actions import encode_email_address +from zerver.lib.email_mirror import logger, process_message +from zerver.lib.send_email import FromAddress +from zerver.lib.management import ZulipBaseCommand + +from zerver.models import Realm, get_stream, get_realm + +from typing import Any, Dict + +# This command loads an email from a specified file and sends it +# to the email mirror. Simple emails can be passed in a JSON file, +# Look at zerver/tests/fixtures/email/1.json for an example of how +# it should look. You can also pass a file which has the raw email, +# for example by writing an email.message.Message type object +# to a file using as_string() or as_bytes() methods, or copy-pasting +# the content of "Show original" on an email in Gmail. +# See zerver/tests/fixtures/email/1.txt for a very simple example, +# but anything that the message_from_binary_file function +# from the email library can parse should work. +# Value of the TO: header doesn't matter, as it is overriden +# by the command in order for the email to be sent to the correct stream. + +class Command(ZulipBaseCommand): + help = """ +Send specified email from a fixture file to the email mirror +Example: +./manage.py send_to_email_mirror --fixture=zerver/tests/fixtures/emails/filename + +""" + + def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument('-f', '--fixture', + dest='fixture', + type=str, + help='The path to the email message you\'d like to send ' + 'to the email mirror.\n' + 'Accepted formats: json or raw email file. ' + 'See zerver/tests/fixtures/email/ for examples') + parser.add_argument('-s', '--stream', + dest='stream', + type=str, + help='The name of the stream to which you\'d like to send ' + 'the message. Default: Denmark') + + self.add_realm_args(parser, help="Specify which realm to connect to; default is zulip") + + def handle(self, **options: str) -> None: + if options['fixture'] is None: + self.print_help('./manage.py', 'send_to_email_mirror') + exit(1) + + if options['stream'] is None: + stream = "Denmark" + else: + stream = options['stream'] + + realm = self.get_realm(options) + if realm is None: + realm = get_realm("zulip") + + full_fixture_path = os.path.join(settings.DEPLOY_ROOT, options['fixture']) + + # parse the input email into Message type and prepare to process_message() it + message = self._parse_email_fixture(full_fixture_path) + self._prepare_message(message, realm, stream) + + process_message(message) + + def _does_fixture_path_exist(self, fixture_path: str) -> bool: + return os.path.exists(fixture_path) + + def _parse_email_json_fixture(self, fixture_path: str) -> Message: + with open(fixture_path) as fp: + json_content = ujson.load(fp)[0] + + message = MIMEText(json_content['body']) + message['From'] = json_content['from'] + message['Subject'] = json_content['subject'] + return message + + def _parse_email_fixture(self, fixture_path: str) -> Message: + if not self._does_fixture_path_exist(fixture_path): + print('Fixture {} does not exist'.format(fixture_path)) + exit(1) + + if fixture_path.endswith('.json'): + message = self._parse_email_json_fixture(fixture_path) + else: + with open(fixture_path, "rb") as fp: + message = email.message_from_binary_file(fp) + + return message + + def _prepare_message(self, message: Message, realm: Realm, stream_name: str) -> None: + stream = get_stream(stream_name, realm) + + recipient_headers = ["X-Gm-Original-To", "Delivered-To", + "Resent-To", "Resent-CC", "To", "CC"] + for header in recipient_headers: + if header in message: + del message[header] + message[header] = encode_email_address(stream) + return + + message['To'] = encode_email_address(stream) diff --git a/zerver/tests/fixtures/email/1.json b/zerver/tests/fixtures/email/1.json new file mode 100644 index 0000000000..96342e66e0 --- /dev/null +++ b/zerver/tests/fixtures/email/1.json @@ -0,0 +1,8 @@ +[ + { + "from": "hamlet@zulip.com", + "to": "foo@zulip.com", + "subject": "1.json", + "body": "Email fixture 1.json body" + } +] diff --git a/zerver/tests/fixtures/email/1.txt b/zerver/tests/fixtures/email/1.txt new file mode 100644 index 0000000000..e966e6c2ed --- /dev/null +++ b/zerver/tests/fixtures/email/1.txt @@ -0,0 +1,9 @@ +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Subject: Normal subject +From: hamlet@zulip.com +To: foo@zulipdev.com +Reply-to: othello@zulip.com + +Email fixture 1.txt body diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index 04bb5f9dd2..c4a7cd49b0 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -3,7 +3,9 @@ import glob import os import re +import email from datetime import timedelta +from email.mime.text import MIMEText from email.utils import parseaddr from mock import MagicMock, patch, call from typing import List, Dict, Any, Optional @@ -16,8 +18,9 @@ from zerver.lib.management import ZulipBaseCommand, CommandError, check_config from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import stdout_suppressed from zerver.lib.test_runner import slow -from zerver.models import get_user_profile_by_email +from zerver.models import Recipient, get_user_profile_by_email, get_stream +from zerver.lib.test_helpers import most_recent_message from zerver.models import get_realm, UserProfile, Realm from confirmation.models import RealmCreationKey, generate_realm_creation_url @@ -295,3 +298,46 @@ class TestRealmReactivationEmail(ZulipTestCase): realm = get_realm('zulip') with self.assertRaisesRegex(CommandError, "The realm %s is already active." % (realm.name,)): call_command(self.COMMAND_NAME, "--realm=zulip") + +class TestSendToEmailMirror(ZulipTestCase): + COMMAND_NAME = "send_to_email_mirror" + + def test_sending_a_fixture(self) -> None: + fixture_path = "zerver/tests/fixtures/email/1.txt" + user_profile = self.example_user('hamlet') + self.login(user_profile.email) + self.subscribe(user_profile, "Denmark") + + call_command(self.COMMAND_NAME, "--fixture={}".format(fixture_path)) + message = most_recent_message(user_profile) + + # last message should be equal to the body of the email in 1.txt + self.assertEqual(message.content, "Email fixture 1.txt body") + + def test_sending_a_json_fixture(self) -> None: + fixture_path = "zerver/tests/fixtures/email/1.json" + user_profile = self.example_user('hamlet') + self.login(user_profile.email) + self.subscribe(user_profile, "Denmark") + + call_command(self.COMMAND_NAME, "--fixture={}".format(fixture_path)) + message = most_recent_message(user_profile) + + # last message should be equal to the body of the email in 1.json + self.assertEqual(message.content, "Email fixture 1.json body") + + def test_stream_option(self) -> None: + fixture_path = "zerver/tests/fixtures/email/1.txt" + user_profile = self.example_user('hamlet') + self.login(user_profile.email) + self.subscribe(user_profile, "Denmark2") + + call_command(self.COMMAND_NAME, "--fixture={}".format(fixture_path), "--stream=Denmark2") + message = most_recent_message(user_profile) + + # last message should be equal to the body of the email in 1.txt + self.assertEqual(message.content, "Email fixture 1.txt body") + + stream_id = get_stream("Denmark2", message.sender.realm).id + self.assertEqual(message.recipient.type, Recipient.STREAM) + self.assertEqual(message.recipient.type_id, stream_id)