Compare commits

...

5 Commits

Author SHA1 Message Date
Alya Abbott
14902688d2 docs and portico: Update documentation word counts. 2025-10-09 15:11:42 -07:00
Lauryn Menard
b42d3e77e7 forms: Set EmailField max_length to match Django Model.EmailField.
Django's Model.EmailField's default max_length is 254 characters,
while the Form.EmailField's default max length is 320 characters.
The longer valid length for form email fields raises an error
when an email with over 254 characters is validated and the server
attempts to create a preregistration user or realm.

Sets the max length on current form EmailFields to match the max
length on corresponding email fields in the database.

For the form MultiEmailField used on the find account/team page,
we don't need to set the max length to 254, but we don't expect
any emails longer than that to match any existing user accounts.
Adds tests in `zerver/tests/test_signup.py` for form submissions
with long email addresses.
2025-10-09 15:47:04 -04:00
Alex Vandiver
fdcfafd13d send_custom_email: Add a flag for sending release announcements. 2025-10-09 12:10:15 -07:00
Alex Vandiver
79e718ed3a send_email: Move break into custom_email_sender. 2025-10-09 12:10:15 -07:00
Vector73
a5d25826bd github_action: Mention PR where the endpoints were added.
Updates "API Documentation Update Check" tool to  add PR
information in the message to chat.zulip.org when the
new endpoints are added.
2025-10-09 11:39:54 -07:00
12 changed files with 137 additions and 27 deletions

View File

@@ -66,6 +66,8 @@ jobs:
- name: Run tools/notify-if-api-docs-changed
id: run_check
run: ./tools/notify-if-api-docs-changed >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Report status to CZO
if: ${{github.repository == 'zulip/zulip'}}

View File

@@ -36,7 +36,7 @@ Come find us on the [development community chat](https://zulip.com/development-c
contributors](https://zulip.readthedocs.io/en/latest/contributing/contributing.html)
to get started. We have invested in making Zulips code highly
readable, thoughtfully tested, and easy to modify. Beyond that, we
have written an extraordinary 150K words of documentation for Zulip
have written an extraordinary 185K words of documentation for Zulip
contributors.
- **Contributing non-code**. [Report an

View File

@@ -30,7 +30,7 @@ your internship experience with the Zulip project will be highly interactive.
> and welcoming. You learn a lot just by watching others work and talk.”_ Sai
> Rohitth Chiluka, Zulip GSoC 2021 participant
As part of our commitment to mentorship, Zulip has over 160,000 words of
As part of our commitment to mentorship, Zulip has over 185,000 words of
[documentation for
developers](../index.md#zulip-documentation-overview), much of it
designed to explain not just how Zulip works, but why Zulip works the way that

View File

@@ -252,7 +252,7 @@
<p>
Zulip makes it easy to
<a href="https://zulip.readthedocs.io/en/stable/production/modify.html">
maintain a fork</a> with customized features, with 175,000 words of documentation for
maintain a fork</a> with customized features, with 225,000 words of documentation for
system administrators and developers.
</p>
</div>

View File

@@ -32,7 +32,7 @@ fully committed to helping bring up the next generation of open-source
contributors from a wide range of backgrounds.
We have invested into making Zulips code uniquely readable, well tested, and
easy to modify. Beyond that, we have written an extraordinary 150K words of
easy to modify. Beyond that, we have written an extraordinary 185K words of
documentation on [how to contribute to
Zulip](https://zulip.readthedocs.io/en/latest/overview/contributing.html), with
topics ranging from [practical Git

View File

@@ -13,9 +13,13 @@
{% block manage_preferences %}
{% if remote_server_email %}
<p>You are receiving this email to update you about important changes to Zulip's Terms of Service.</p>
<a href="{{ unsubscribe_link }}">Unsubscribe</a>
{% if released_version %}
<p>You are receiving this email because you opted into release notifications.</p>
{% else %}
<p>You are receiving this email to update you about important changes to Zulip's Terms of Service.</p>
<a href="{{ unsubscribe_link }}">Unsubscribe</a>
{% endif %}
{% elif unsubscribe_link %}
<p><a href="{{ realm_url }}/#settings/notifications">{{ _("Manage email preferences") }}</a> | <a href="{{ unsubscribe_link }}">{{ _("Unsubscribe from marketing emails") }}</a></p>
<p><a href="{{ realm_url }}/#settings/notifications">{{ _("Manage email preferences") }}</a> | <a href="{{ unsubscribe_link }}">{{ _("Unsubscribe from marketing emails") }}</a></p>
{% endif %}
{% endblock %}

View File

@@ -1,9 +1,13 @@
---
{% if remote_server_email %}
{% if released_version %}
You are receiving this email because you opted into release notifications.
{% else %}
You are receiving this email to update you about important changes to Zulip's Terms of Service.
Unsubscribe: {{ unsubscribe_link }}
{% endif %}
{% elif unsubscribe_link %}
{{ _("Manage email preferences") }}:

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
from pathlib import Path
from urllib.request import Request, urlopen
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(os.path.dirname(TOOLS_DIR))
@@ -11,11 +13,26 @@ sys.path.insert(0, os.path.dirname(TOOLS_DIR))
from zerver.openapi.merge_api_changelogs import get_feature_level
def get_build_url_from_environment() -> str:
server = os.environ["GITHUB_SERVER_URL"]
def get_pull_request_number_or_commit_hash() -> str:
github_token = os.environ["GITHUB_TOKEN"]
repo = os.environ["GITHUB_REPOSITORY"]
run_id = os.environ["GITHUB_RUN_ID"]
return f"{server}/{repo}/actions/runs/{run_id}"
commit_hash = os.environ["GITHUB_SHA"]
url = f"https://api.github.com/repos/{repo}/commits/{commit_hash}/pulls"
headers = {
"Accept": "application/vnd.github.groot-preview+json",
"Authorization": f"token {github_token}",
}
try:
req = Request(url, headers=headers)
with urlopen(req) as response:
pull_requests = json.load(response)
if len(pull_requests) > 0:
return f"#{pull_requests[0]['number']}"
return commit_hash
except Exception:
return commit_hash
def get_changed_api_endpoints() -> list[str]:
@@ -41,15 +58,12 @@ def get_changed_api_endpoints() -> list[str]:
if __name__ == "__main__":
branch = os.environ.get("GITHUB_REF", "unknown branch").split("/")[-1]
topic = f"{branch} failing"
build_url = get_build_url_from_environment()
github_actor = os.environ.get("GITHUB_ACTOR", "unknown user")
pull_request = get_pull_request_number_or_commit_hash()
feature_level = get_feature_level(update_feature_level=False)
endpoints = get_changed_api_endpoints()
topic = f"new feature level: {feature_level}"
endpoints_string = ", ".join(endpoints)
content = f"[Build]({build_url}) triggered by {github_actor} on branch `{branch}` has updated the API documentation for the following endpoints: {endpoints_string}."
content = f"{pull_request} has updated the API documentation for the following endpoints: {endpoints_string}."
print(f"topic={topic}\ncontent={content}")

View File

@@ -68,6 +68,11 @@ DEACTIVATED_ACCOUNT_ERROR = gettext_lazy(
)
PASSWORD_TOO_WEAK_ERROR = gettext_lazy("The password is too weak.")
# Set Form.EmailField to match the default max_length on Model.EmailField,
# can be removed when https://code.djangoproject.com/ticket/35119 is
# completed.
EMAIL_MAX_LENGTH = 254
class OverridableValidationError(ValidationError):
pass
@@ -242,7 +247,7 @@ class ToSForm(forms.Form):
class HomepageForm(forms.Form):
email = forms.EmailField()
email = forms.EmailField(max_length=EMAIL_MAX_LENGTH)
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.realm = kwargs.pop("realm", None)
@@ -321,7 +326,9 @@ class ImportRealmOwnerSelectionForm(forms.Form):
class RealmCreationForm(RealmDetailsForm):
# This form determines whether users can create a new realm.
email = forms.EmailField(validators=[email_not_system_bot, email_is_not_disposable])
email = forms.EmailField(
validators=[email_not_system_bot, email_is_not_disposable], max_length=EMAIL_MAX_LENGTH
)
import_from = forms.ChoiceField(
choices=PreregistrationRealm.IMPORT_FROM_CHOICES,
required=False,
@@ -539,7 +546,7 @@ def rate_limit_password_reset_form_by_email(email: str) -> None:
class CreateUserForm(forms.Form):
full_name = forms.CharField(max_length=100)
email = forms.EmailField()
email = forms.EmailField(max_length=EMAIL_MAX_LENGTH)
class OurAuthenticationForm(AuthenticationForm):

View File

@@ -654,10 +654,18 @@ def custom_email_sender(
with open(subject_path, "w") as f:
f.write(get_header(subject, parsed_email_template.get("subject"), "subject"))
already_printed_once = False
def send_one_email(
context: dict[str, Any], to_user_id: int | None = None, to_email: str | None = None
) -> None:
assert to_user_id is not None or to_email is not None
if dry_run:
nonlocal already_printed_once
if already_printed_once:
return
else:
already_printed_once = True
with suppress(EmailNotDeliveredError):
send_immediate_email(
email_id,
@@ -715,9 +723,6 @@ def send_custom_email(
to_user_id=user_profile.id,
context=context,
)
if dry_run:
break
return users
@@ -753,9 +758,6 @@ def send_custom_server_email(
context=context,
)
if dry_run:
break
def log_email_config_errors() -> None:
"""

View File

@@ -9,11 +9,12 @@ from typing_extensions import override
from confirmation.models import one_click_unsubscribe_link
from zerver.lib.management import ZulipBaseCommand
from zerver.lib.send_email import send_custom_email, send_custom_server_email
from zerver.lib.send_email import custom_email_sender, send_custom_email, send_custom_server_email
from zerver.models import Realm, UserProfile
if settings.ZILENCER_ENABLED:
from zilencer.models import RemoteZulipServer
from corporate.lib.stripe import BILLING_SUPPORT_EMAIL
from zilencer.models import RemoteRealmBillingUser, RemoteServerBillingUser, RemoteZulipServer
class Command(ZulipBaseCommand):
@@ -41,6 +42,11 @@ class Command(ZulipBaseCommand):
action="store_true",
help="Send to registered contact email addresses for remote Zulip servers.",
)
targets.add_argument(
"--announce-release",
metavar="VERSION",
help="Announce a major or minor release to remote servers.",
)
targets.add_argument(
"--all-sponsored-org-admins",
action="store_true",
@@ -133,6 +139,45 @@ class Command(ZulipBaseCommand):
for server in servers:
print(f" {server.contact_email} ({server.hostname})")
return
elif options["announce_release"]:
server_users = RemoteServerBillingUser.objects.filter(
is_active=True,
remote_server__deactivated=False,
)
realm_users = RemoteRealmBillingUser.objects.filter(
is_active=True,
remote_realm__server__deactivated=False,
remote_realm__is_system_bot_realm=False,
remote_realm__registration_deactivated=False,
remote_realm__realm_deactivated=False,
remote_realm__realm_locally_deleted=False,
)
if options["announce_release"].endswith(".0"):
server_users = server_users.filter(enable_major_release_emails=True)
realm_users = realm_users.filter(enable_major_release_emails=True)
else:
server_users = server_users.filter(enable_maintenance_release_emails=True)
realm_users = realm_users.filter(enable_maintenance_release_emails=True)
# This does an implicit "distinct"
all_emails = server_users.union(realm_users).values_list("email", flat=True)
del options["from_address"]
email_sender = custom_email_sender(
dry_run=dry_run, from_address=BILLING_SUPPORT_EMAIL, **options
)
for email in all_emails:
email_sender(
to_email=email,
context={
"remote_server_email": True,
"released_version": options["announce_release"],
},
)
if dry_run:
print("Would send the above email to:")
for email in all_emails:
print(f" {email}")
return
if options["entire_server"]:
users = UserProfile.objects.filter(

View File

@@ -1108,6 +1108,11 @@ class LoginTest(ZulipTestCase):
self.assertEqual(result.status_code, 200)
self.assertContains(result, "Enter a valid email address")
invalid_email = "a" * 260 + "@example.com"
result = self.client_post("/accounts/home/", {"email": invalid_email}, subdomain="zulip")
self.assertEqual(result.status_code, 200)
self.assertContains(result, "Ensure this value has at most 254 characters (it has 272).")
def test_register_deactivated_partway_through(self) -> None:
"""
If you try to register for a deactivated realm, you get a clear error
@@ -4743,6 +4748,33 @@ class TestFindMyTeam(ZulipTestCase):
result = self.client_get("/accounts/find/", {"emails": "invalid"})
self.assertEqual(result.status_code, 200)
def test_find_team_long_email_address(self) -> None:
# Emails over 320 characters are considered invalid.
data = {"emails": "a" * 320 + "@example.com"}
result = self.client_post("/accounts/find/", data)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Enter a valid email", result.content)
from django.core.mail import outbox
self.assert_length(outbox, 0)
# Emails in the database are never over 254 characters,
# but searching for them does not cause an error.
# When https://code.djangoproject.com/ticket/35119 is
# resolved, Django's email validator will return this
# case as invalid, so this test will need to be updated.
data = {"emails": "a" * 260 + "@example.com"}
result = self.client_post("/accounts/find/", data)
self.assertEqual(result.status_code, 200)
content = result.content.decode()
self.assertIn("Emails sent! The addresses entered on", content)
self.assertIn("a@example.com", content)
from django.core.mail import outbox
self.assert_length(outbox, 1)
message = outbox[0]
self.assertIn("Unfortunately, no Zulip Cloud accounts", message.body)
def test_find_team_zero_emails(self) -> None:
data = {"emails": ""}
result = self.client_post("/accounts/find/", data)