diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 98c70d70dd..a4b185c0dd 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -31,6 +31,10 @@ format used by the Zulip server that they are interacting with. `email_address` field from subscription objects. This change was backported from Zulip 8.0, where it was introduced in feature level 226. +* [`GET /streams/{stream_id}/email_address`](/api/get-stream-email-address): + Added new endpoint to get email address of a stream. This change was + backported from Zulip 8.0, where it was introduced in feature level 226. + ## Changes in Zulip 7.0 **Feature level 185** diff --git a/version.py b/version.py index 1705c84e9d..59c97e44b6 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.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 = 185 +API_FEATURE_LEVEL = 186 # 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.js b/web/src/stream_data.js index 079306ad91..20af305c31 100644 --- a/web/src/stream_data.js +++ b/web/src/stream_data.js @@ -496,6 +496,13 @@ export function can_toggle_subscription(sub) { ); } +export function can_access_stream_email(sub) { + 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) { // 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 a98732946a..51d1b439f0 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -240,6 +240,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); @@ -400,6 +401,66 @@ 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); + + new ClipboardJS("#copy_email_address_modal .dialog_submit_button", { + text() { + return address; + }, + }); + + $("#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(); @@ -480,64 +541,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"}), - help_link: "/help/message-a-stream-by-email#configuration-options", - on_click() {}, - close_on_submit: true, - }); - $("#show-sender").prop("checked", true); - - 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"), + ); }, - }); - - $("#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 0ca5e0264e..46fc07f39b 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -943,8 +943,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 924038b906..8fe2988dcf 100644 --- a/web/templates/stream_settings/stream_settings.hbs +++ b/web/templates/stream_settings/stream_settings.hbs @@ -64,11 +64,14 @@ can_remove_subscribers_setting_widget_name="can_remove_subscribers_group_id" }} {{/with}} -
-

- {{t "Email address" }} - {{> ../help_link_widget link="/help/message-a-stream-by-email" }} -

+
+

{{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 fdff41c78d..431cc21bff 100644 --- a/web/tests/stream_data.test.js +++ b/web/tests/stream_data.test.js @@ -981,3 +981,44 @@ test("can_unsubscribe_others", () => { page_params.is_admin = false; assert.equal(stream_data.can_unsubscribe_others(sub), false); }); + +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 5813e861ad..283eadb5e9 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -15687,6 +15687,55 @@ 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 7.5 (feature level 186). + 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/JsonError" + - example: + { + "code": "BAD_REQUEST", + "msg": "Invalid stream ID", + "result": "error", + } + description: | + An example JSON response for when the supplied stream does not exist: /streams/{stream_id}/delete_topic: post: operationId: delete-topic diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 75ddb8c48d..f1aed2104b 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -31,12 +31,14 @@ 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, ) from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group from zerver.actions.users import do_change_user_role, do_deactivate_user +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 @@ -5611,6 +5613,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 04e1dea26b..48832939a5 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -47,6 +47,7 @@ from zerver.decorator import ( require_post, require_realm_admin, ) +from zerver.lib.email_mirror_helpers import encode_email_address from zerver.lib.exceptions import ( ErrorCode, JsonableError, @@ -1073,3 +1074,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 5a43157d67..3d743c4436 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -146,6 +146,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, @@ -464,6 +465,7 @@ v1_api_and_json_patterns = [ PATCH=update_stream_backend, DELETE=deactivate_stream_backend, ), + rest_path("streams//email_address", GET=get_stream_email_address), # Delete topic in stream rest_path("streams//delete_topic", POST=delete_in_topic), rest_path("default_streams", POST=add_default_stream, DELETE=remove_default_stream),