Skip to content

Commit

Permalink
streams: Add API endpoint to get stream email.
Browse files Browse the repository at this point in the history
This commit adds new API endpoint to get stream email which is
used by the web-app as well to get the email when a user tries
to open the stream email modal.

The stream email is returned only to the users who have access
to it. Specifically for private streams only subscribed users
have access to its email. And for public streams, all non-guest
users and only subscribed guests have access to its email.
All users can access email of web-public streams.
  • Loading branch information
sahil839 authored and alexmv committed Nov 16, 2023
1 parent 0a38003 commit 6e11984
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 64 deletions.
4 changes: 4 additions & 0 deletions api_docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# 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
Expand Down
7 changes: 7 additions & 0 deletions web/src/stream_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
129 changes: 73 additions & 56 deletions web/src/stream_edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
});

Expand Down
11 changes: 9 additions & 2 deletions web/styles/subscriptions.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 8 additions & 5 deletions web/templates/stream_settings/stream_settings.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,14 @@
can_remove_subscribers_setting_widget_name="can_remove_subscribers_group_id" }}
</div>
{{/with}}
<div class="stream-email-box" {{#unless sub.email_address}}style="display: none;"{{/unless}}>
<h3 class="stream_setting_subsection_title">
{{t "Email address" }}
{{> ../help_link_widget link="/help/message-a-stream-by-email" }}
</h3>
<div class="stream-email-box" {{#unless can_access_stream_email}}style="display: none;"{{/unless}}>
<div class="stream-email-box-header">
<h3 class="stream_setting_subsection_title">
{{t "Email address" }}
{{> ../help_link_widget link="/help/message-a-stream-by-email" }}
</h3>
<div class="stream_email_address_error alert-notification"></div>
</div>
<p>
{{t "You can use email to send messages to Zulip streams."}}
</p>
Expand Down
41 changes: 41 additions & 0 deletions web/tests/stream_data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
49 changes: 49 additions & 0 deletions zerver/openapi/zulip.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions zerver/tests/test_subs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
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
Expand Down Expand Up @@ -5611,6 +5613,54 @@ def test_get_single_stream_api(self) -> None:
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:
Expand Down

0 comments on commit 6e11984

Please sign in to comment.