Repository: zulip/zulip

Issue:
Failures to send message in email gateway should notify the user in some way
Replying to an email on, say, an announcements stream (which only allows admins to post) will attempt to reply on the stream, and error out in `_internal_prep_message` due to the authorization check in `check_message`.

This results in logging an exception, returning `None` from `internal_send_stream_message` in `process_missed_message`, ignoring that value, and then logging `Successfully processed email from user`.  From the user's point of view, they receive no feedback that the message failed to send, or why.

If the message is from the email gateway, we should not log an exception for auth failures; rather, we should notify the user (either via a reply email, or via a PM) that their message failed to send.


Discussion:
Hello @zulip/server-development members, this issue was labeled with the "area: emails" label, so you may want to check it out!

<!-- areaLabelAddition -->
@timabbott I was looking to work on this issue. Shall I go ahead?
@zulipbot claim
Hi @Abhirup-99, I was actually having a start at this issue but could not have a decent progress as my college exams and practicals kicked in. I would like to carry on with this one after the exams if you don't mind. I hope you can find some other good issue to work on. Btw zulip-bot is down these days, so you may not be able to claim issues.
Some chat discussion here: https://chat.zulip.org/#narrow/stream/127-integrations/topic/email-gateway/near/1084269
I wanted to to work on it, @ligmitz are you still working on it?

@m-e-l-u-h-a-n Working on it !

Resolving patch:
diff --git a/zerver/lib/email_mirror.py b/zerver/lib/email_mirror.py
--- a/zerver/lib/email_mirror.py
+++ b/zerver/lib/email_mirror.py
@@ -10,6 +10,7 @@
 from django.utils.timezone import timedelta
 
 from zerver.lib.actions import (
+    check_send_message,
     internal_send_huddle_message,
     internal_send_private_message,
     internal_send_stream_message,
@@ -20,7 +21,7 @@
     get_email_gateway_message_string_from_address,
 )
 from zerver.lib.email_notifications import convert_html_to_markdown
-from zerver.lib.exceptions import RateLimited
+from zerver.lib.exceptions import JsonableError, RateLimited
 from zerver.lib.message import normalize_body, truncate_topic
 from zerver.lib.queue import queue_json_publish
 from zerver.lib.rate_limiter import RateLimitedObject
@@ -33,6 +34,7 @@
     Recipient,
     Stream,
     UserProfile,
+    get_client,
     get_display_recipient,
     get_stream_by_id_in_realm,
     get_system_bot,
@@ -210,6 +212,27 @@ def send_zulip(sender: UserProfile, stream: Stream, topic: str, content: str) ->
     )
 
 
+def send_mm_reply_to_stream(
+    user_profile: UserProfile, stream: Stream, topic: str, body: str
+) -> None:
+    try:
+        check_send_message(
+            sender=user_profile,
+            client=get_client("Internal"),
+            message_type_name="stream",
+            message_to=[stream.id],
+            topic_name=topic,
+            message_content=body,
+        )
+    except JsonableError as error:
+        error_message = "Error sending message to stream {stream} via missed messages email reply:\n{error}".format(
+            stream=stream.name, error=error.msg
+        )
+        internal_send_private_message(
+            get_system_bot(settings.NOTIFICATION_BOT), user_profile, error_message
+        )
+
+
 def get_message_part_by_type(message: EmailMessage, content_type: str) -> Optional[str]:
     charsets = message.get_charsets()
 
@@ -429,12 +452,7 @@ def process_missed_message(to: str, message: EmailMessage) -> None:
 
     if recipient.type == Recipient.STREAM:
         stream = get_stream_by_id_in_realm(recipient.type_id, user_profile.realm)
-        internal_send_stream_message(
-            user_profile,
-            stream,
-            topic,
-            body,
-        )
+        send_mm_reply_to_stream(user_profile, stream, topic, body)
         recipient_str = stream.name
     elif recipient.type == Recipient.PERSONAL:
         display_recipient = get_display_recipient(recipient)


Test patch:
diff --git a/zerver/tests/test_email_mirror.py b/zerver/tests/test_email_mirror.py
--- a/zerver/tests/test_email_mirror.py
+++ b/zerver/tests/test_email_mirror.py
@@ -11,7 +11,12 @@
 from django.conf import settings
 from django.http import HttpResponse
 
-from zerver.lib.actions import do_deactivate_realm, do_deactivate_user, ensure_stream
+from zerver.lib.actions import (
+    do_change_stream_post_policy,
+    do_deactivate_realm,
+    do_deactivate_user,
+    ensure_stream,
+)
 from zerver.lib.email_mirror import (
     ZulipEmailForwardError,
     create_missed_message_address,
@@ -37,6 +42,7 @@
 from zerver.models import (
     MissedMessageEmailAddress,
     Recipient,
+    Stream,
     UserProfile,
     get_display_recipient,
     get_realm,
@@ -927,6 +933,47 @@ def test_receive_missed_stream_message_email_messages(self) -> None:
         self.assertEqual(message.recipient.type, Recipient.STREAM)
         self.assertEqual(message.recipient.id, usermessage.message.recipient.id)
 
+    def test_receive_email_response_for_auth_failures(self) -> None:
+        user_profile = self.example_user("hamlet")
+        self.subscribe(user_profile, "announce")
+        self.login("hamlet")
+        result = self.client_post(
+            "/json/messages",
+            {
+                "type": "stream",
+                "topic": "test topic",
+                "content": "test_receive_email_response_for_auth_failures",
+                "client": "test suite",
+                "to": "announce",
+            },
+        )
+        self.assert_json_success(result)
+
+        stream = get_stream("announce", user_profile.realm)
+        do_change_stream_post_policy(stream, Stream.STREAM_POST_POLICY_ADMINS)
+
+        usermessage = most_recent_usermessage(user_profile)
+
+        mm_address = create_missed_message_address(user_profile, usermessage.message)
+
+        incoming_valid_message = EmailMessage()
+        incoming_valid_message.set_content("TestMissedMessageEmailMessages Body")
+
+        incoming_valid_message["Subject"] = "TestMissedMessageEmailMessages Subject"
+        incoming_valid_message["From"] = user_profile.delivery_email
+        incoming_valid_message["To"] = mm_address
+        incoming_valid_message["Reply-to"] = user_profile.delivery_email
+
+        process_message(incoming_valid_message)
+
+        message = most_recent_message(user_profile)
+
+        self.assertEqual(
+            message.content,
+            "Error sending message to stream announce via missed messages email reply:\nOnly organization administrators can send to this stream.",
+        )
+        self.assertEqual(message.sender, get_system_bot(settings.NOTIFICATION_BOT))
+
     def test_missed_stream_message_email_response_tracks_topic_change(self) -> None:
         self.subscribe(self.example_user("hamlet"), "Denmark")
         self.subscribe(self.example_user("othello"), "Denmark")