diff --git a/api_docs/changelog.md b/api_docs/changelog.md index c39a4e0f11..8aa4bda213 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -26,6 +26,9 @@ format used by the Zulip server that they are interacting with. [`GET /users/me/subscriptions`](/api/get-subscriptions): Removed `email_address` field from subscription objects. +* [`GET /streams/{stream_id}/email_address`](/api/get-stream-email-address): + Added new endpoint to get email address of a stream. + **Feature level 225** * `PATCH /realm`, [`POST /register`](/api/register-queue), diff --git a/version.py b/version.py index 76b6265ac2..a2a250d66c 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 225 +API_FEATURE_LEVEL = 226 # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index 3cd3f8f3c2..0bcc543729 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -502,6 +502,13 @@ export function can_toggle_subscription(sub: StreamSubscription): boolean { ); } +export function can_access_stream_email(sub: StreamSubscription): boolean { + return ( + (sub.subscribed || sub.is_web_public || (!page_params.is_guest && !sub.invite_only)) && + !page_params.is_spectator + ); +} + export function can_access_topic_history(sub: StreamSubscription): boolean { // Anyone can access topic history for web-public streams and // subscriptions; additionally, members can access history for diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index 36882ab718..329e6bb525 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -259,6 +259,7 @@ export function show_settings_for(node) { page_params.realm_org_type === settings_config.all_org_type_values.business.code, is_admin: page_params.is_admin, org_level_message_retention_setting: get_display_text_for_realm_message_retention_setting(), + can_access_stream_email: stream_data.can_access_stream_email(sub), }); scroll_util.get_content_element($("#stream_settings")).html(html); @@ -352,6 +353,71 @@ export function get_stream_email_address(flags, address) { return clean_address.replace("@", flag_string + "@"); } +function show_stream_email_address_modal(address) { + const copy_email_address_modal_html = render_copy_email_address_modal({ + email_address: address, + tags: [ + { + name: "show-sender", + description: $t({ + defaultMessage: "The sender's email address", + }), + }, + { + name: "include-footer", + description: $t({defaultMessage: "Email footers (e.g., signature)"}), + }, + { + name: "include-quotes", + description: $t({defaultMessage: "Quoted original email (in replies)"}), + }, + { + name: "prefer-html", + description: $t({ + defaultMessage: "Use html encoding (not recommended)", + }), + }, + ], + }); + + dialog_widget.launch({ + html_heading: $t_html({defaultMessage: "Generate stream email address"}), + html_body: copy_email_address_modal_html, + id: "copy_email_address_modal", + html_submit_button: $t_html({defaultMessage: "Copy address"}), + html_exit_button: $t_html({defaultMessage: "Close"}), + help_link: "/help/message-a-stream-by-email#configuration-options", + on_click() {}, + close_on_submit: false, + }); + $("#show-sender").prop("checked", true); + + const clipboard = new ClipboardJS("#copy_email_address_modal .dialog_submit_button", { + text() { + return address; + }, + }); + + // Show a tippy tooltip when the stream email address copied + clipboard.on("success", (e) => { + show_copied_confirmation(e.trigger); + }); + + $("#copy_email_address_modal .tag-checkbox").on("change", () => { + const $checked_checkboxes = $(".copy-email-modal").find("input:checked"); + + const flags = []; + + $($checked_checkboxes).each(function () { + flags.push($(this).attr("id")); + }); + + address = get_stream_email_address(flags, address); + + $(".email-address").text(address); + }); +} + export function initialize() { $("#main_div").on("click", ".stream_sub_unsub_button", (e) => { e.preventDefault(); @@ -457,70 +523,20 @@ export function initialize() { e.stopPropagation(); const stream_id = get_stream_id(e.target); - const stream = sub_store.get(stream_id); - let address = stream.email_address; - const copy_email_address = render_copy_email_address_modal({ - email_address: address, - tags: [ - { - name: "show-sender", - description: $t({ - defaultMessage: "The sender's email address", - }), - }, - { - name: "include-footer", - description: $t({defaultMessage: "Email footers (e.g., signature)"}), - }, - { - name: "include-quotes", - description: $t({defaultMessage: "Quoted original email (in replies)"}), - }, - { - name: "prefer-html", - description: $t({ - defaultMessage: "Use html encoding (not recommended)", - }), - }, - ], - }); - - dialog_widget.launch({ - html_heading: $t_html({defaultMessage: "Generate stream email address"}), - html_body: copy_email_address, - id: "copy_email_address_modal", - html_submit_button: $t_html({defaultMessage: "Copy address"}), - html_exit_button: $t_html({defaultMessage: "Close"}), - help_link: "/help/message-a-stream-by-email#configuration-options", - on_click() {}, - close_on_submit: false, - }); - $("#show-sender").prop("checked", true); - - const clipboard = new ClipboardJS("#copy_email_address_modal .dialog_submit_button", { - text() { - return address; + channel.get({ + url: "/json/streams/" + stream_id + "/email_address", + success(data) { + const address = data.email; + show_stream_email_address_modal(address); + }, + error(xhr) { + ui_report.error( + $t_html({defaultMessage: "Failed"}), + xhr, + $(".stream_email_address_error"), + ); }, - }); - - // Show a tippy tooltip when the stream email address copied - clipboard.on("success", (e) => { - show_copied_confirmation(e.trigger); - }); - - $("#copy_email_address_modal .tag-checkbox").on("change", () => { - const $checked_checkboxes = $(".copy-email-modal").find("input:checked"); - - const flags = []; - - $($checked_checkboxes).each(function () { - flags.push($(this).attr("id")); - }); - - address = get_stream_email_address(flags, address); - - $(".email-address").text(address); }); }); diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 592d7e6d36..ea79c718ea 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -946,8 +946,15 @@ h4.user_group_setting_subsection_title { } } - .copy_email_button { - padding: 10px 15px; + .stream-email-box { + .stream_email_address_error { + vertical-align: top; + margin-left: 15px; + } + + .copy_email_button { + padding: 10px 15px; + } } .loading_indicator_text { diff --git a/web/templates/stream_settings/stream_settings.hbs b/web/templates/stream_settings/stream_settings.hbs index 9a6bad9b65..bc52387cd3 100644 --- a/web/templates/stream_settings/stream_settings.hbs +++ b/web/templates/stream_settings/stream_settings.hbs @@ -75,11 +75,14 @@ can_remove_subscribers_setting_widget_name="can_remove_subscribers_group" }} {{/with}} -
{{t "You can use email to send messages to Zulip streams."}}
diff --git a/web/tests/stream_data.test.js b/web/tests/stream_data.test.js index 217fd13333..f5f0fd6ebe 100644 --- a/web/tests/stream_data.test.js +++ b/web/tests/stream_data.test.js @@ -1150,3 +1150,44 @@ test("options for dropdown widget", () => { }, ]); }); + +test("can_access_stream_email", () => { + const social = { + subscribed: true, + color: "red", + name: "social", + stream_id: 2, + is_muted: false, + invite_only: true, + history_public_to_subscribers: false, + }; + page_params.is_admin = false; + assert.equal(stream_data.can_access_stream_email(social), true); + + page_params.is_admin = true; + assert.equal(stream_data.can_access_stream_email(social), true); + + social.subscribed = false; + assert.equal(stream_data.can_access_stream_email(social), false); + + social.invite_only = false; + assert.equal(stream_data.can_access_stream_email(social), true); + + page_params.is_admin = false; + assert.equal(stream_data.can_access_stream_email(social), true); + + page_params.is_guest = true; + assert.equal(stream_data.can_access_stream_email(social), false); + + social.subscribed = true; + assert.equal(stream_data.can_access_stream_email(social), true); + + social.is_web_public = true; + assert.equal(stream_data.can_access_stream_email(social), true); + + social.subscribed = false; + assert.equal(stream_data.can_access_stream_email(social), true); + + page_params.is_spectator = true; + assert.equal(stream_data.can_access_stream_email(social), false); +}); diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 2126004008..57dacb9529 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -16846,6 +16846,50 @@ paths: description: | An example JSON response for when invalid combination of stream permission parameters are passed: + /streams/{stream_id}/email_address: + get: + operationId: get-stream-email-address + summary: Get the email address of a stream + tags: ["streams"] + description: | + Get email address of a stream. + + **Changes**: New in Zulip 8.0 (feature level 226). + parameters: + - $ref: "#/components/parameters/StreamIdInPath" + responses: + "200": + description: Success. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - additionalProperties: false + properties: + result: {} + msg: {} + ignored_parameters_unsupported: {} + email: + type: string + description: | + Email address of the stream. + example: + { + "result": "success", + "msg": "", + "email": "test_stream.af64447e9e39374841063747ade8e6b0.show-sender@testserver", + } + "400": + description: Bad request. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/InvalidStreamError" + - description: | + An example JSON response for when the requested stream does not exist, + or where the user does not have permission to access the target stream: /streams/{stream_id}/delete_topic: post: operationId: delete-topic diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 5fdeedb5d5..ec3a47d180 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -31,6 +31,7 @@ from zerver.actions.streams import ( bulk_remove_subscriptions, deactivated_streams_by_old_name, do_change_stream_group_based_setting, + do_change_stream_permission, do_change_stream_post_policy, do_deactivate_stream, do_unarchive_stream, @@ -41,6 +42,7 @@ from zerver.lib.default_streams import ( get_default_stream_ids_for_realm, get_default_streams_for_realm_as_dicts, ) +from zerver.lib.email_mirror_helpers import encode_email_address_helper from zerver.lib.exceptions import JsonableError from zerver.lib.message import UnreadStreamInfo, aggregate_unread_data, get_raw_unread_data from zerver.lib.response import json_success @@ -5729,6 +5731,54 @@ class GetStreamsTest(ZulipTestCase): self.assertEqual(json["stream"]["name"], "private_stream") self.assertEqual(json["stream"]["stream_id"], private_stream.id) + def test_get_stream_email_address(self) -> None: + self.login("hamlet") + hamlet = self.example_user("hamlet") + iago = self.example_user("iago") + polonius = self.example_user("polonius") + realm = get_realm("zulip") + denmark_stream = get_stream("Denmark", realm) + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + json = self.assert_json_success(result) + denmark_email = encode_email_address_helper( + denmark_stream.name, denmark_stream.email_token, show_sender=True + ) + self.assertEqual(json["email"], denmark_email) + + self.login("polonius") + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + self.assert_json_error(result, "Invalid stream ID") + + self.subscribe(polonius, "Denmark") + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + json = self.assert_json_success(result) + self.assertEqual(json["email"], denmark_email) + + do_change_stream_permission( + denmark_stream, + invite_only=True, + history_public_to_subscribers=True, + is_web_public=False, + acting_user=iago, + ) + self.login("hamlet") + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + json = self.assert_json_success(result) + self.assertEqual(json["email"], denmark_email) + + self.unsubscribe(hamlet, "Denmark") + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + self.assert_json_error(result, "Invalid stream ID") + + self.login("iago") + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + json = self.assert_json_success(result) + self.assertEqual(json["email"], denmark_email) + + self.unsubscribe(iago, "Denmark") + result = self.client_get(f"/json/streams/{denmark_stream.id}/email_address") + self.assert_json_error(result, "Invalid stream ID") + class StreamIdTest(ZulipTestCase): def test_get_stream_id(self) -> None: diff --git a/zerver/views/streams.py b/zerver/views/streams.py index d6b7b17bdd..ba4b1e7d44 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -46,6 +46,7 @@ from zerver.decorator import ( require_post, require_realm_admin, ) +from zerver.lib.email_mirror_helpers import encode_email_address from zerver.lib.default_streams import get_default_stream_ids_for_realm from zerver.lib.exceptions import ( JsonableError, @@ -1085,3 +1086,18 @@ def update_subscription_properties_backend( ) return json_success(request) + + +@has_request_variables +def get_stream_email_address( + request: HttpRequest, + user_profile: UserProfile, + stream_id: int = REQ("stream", converter=to_non_negative_int, path_only=True), +) -> HttpResponse: + (stream, sub) = access_stream_by_id( + user_profile, + stream_id, + ) + stream_email = encode_email_address(stream, show_sender=True) + + return json_success(request, data={"email": stream_email}) diff --git a/zproject/urls.py b/zproject/urls.py index 1bdb3fe316..6610c4d16b 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -149,6 +149,7 @@ from zerver.views.streams import ( deactivate_stream_backend, delete_in_topic, get_stream_backend, + get_stream_email_address, get_streams_backend, get_subscribers_backend, get_topics_backend, @@ -467,6 +468,7 @@ v1_api_and_json_patterns = [ PATCH=update_stream_backend, DELETE=deactivate_stream_backend, ), + rest_path("streams/