My First Contribution to Zulip: Fixing Empty Topic Display in Email Notifications (#36689)

1. Introduction – Why Zulip, Why This Issue
After contributing to Ghost's open-source codebase and learning the process of distributed collaboration, I wanted to explore a different type of project. Zulip caught my attention because of its unique approach to team communication and its well-documented, beginner-friendly contribution process.
What attracted me to Zulip was its thoughtful architecture – a Django backend with modern frontend tooling, comprehensive testing, and a strong focus on code quality. The project felt mature yet welcoming to newcomers.
I picked issue #36689 because it was labelled as a help-wanted issue and involved email notifications – a feature that directly impacts user experience. The problem was clear: empty topics in digest emails were showing raw HTML instead of the expected "general chat" display.
2. Understanding the Problem
The issue affected users receiving email digest notifications when:
Messages were posted to topics with empty names
The digest email would display raw HTML like
<span class="empty-topic-display">general chat</span>Instead of the clean, localised "general chat" text
This created a poor user experience and made Zulip look unprofessional in email clients. The problem occurred specifically in digest emails, not in the web interface, where empty topics displayed correctly.
Expected behaviour: Clean "general chat" text.
Actual behaviour: Raw HTML markup visible to users
3. Reproducing the Issue Locally
I set up my local Zulip development environment following their excellent documentation:
./tools/provision
./tools/run-dev
To reproduce the issue, I:
Created messages in topics with empty names
Navigated to
http://localhost:9991/digest/to preview digest emailsConfirmed the raw HTML was visible in the email preview
Checked both HTML and plain text versions of the digest
The issue was clearly visible in the digest preview, making it easy to verify the problem and later test the fix.
4. Visual Evidence: Before and After
Before Fix - Raw HTML Displayed
| Email Preview | Issue |
Raw HTML <span class="empty-topic-display">general chat</span> visible to users |


After Fix - Clean Display
| Email Preview | Fixed |
| Clean "general chat" text properly rendered |


The difference is immediately apparent - users now see professional, clean email formatting instead of confusing HTML markup.
5. Tracing the Root Cause
At first, I thought this was a simple templating issue in the email templates. I spent time looking through the digest email templates, expecting to find incorrect HTML escaping.
After reading through the codebase more carefully, I realised the issue was deeper in the email notification logic. The problem was in zerver/lib/email_notifications.py in the digest_block_header function.
The actual root cause was a type annotation issue. The code was creating Markup objects inconsistently:
For empty topics: using
Markup().format()with proper escapingFor non-empty topics: directly wrapping in
Markup()without escaping
This inconsistency meant the type checker couldn't properly validate the code, and the HTML rendering behaved differently for empty vs non-empty topics.
6. The Fix (What I Changed and Why)
I needed to ensure both empty and non-empty topics were created Markup objects consistently while maintaining proper HTML escaping.
The solution was to:
Use
escape()to safely handle user input for topic namesCreate
Markupobjects consistently for both casesMaintain the special styling for empty topics with the CSS class
I chose this approach because:
It maintains security by properly escaping user input
It fixes the type annotation issues
It keeps the existing functionality for empty topic styling
It's minimal and doesn't change the overall architecture
The key insight was that Markup().format() auto-escapes parameters, but directly wrapping strings in Markup() bypasses escaping – a potential XSS vulnerability.
7. Testing the Fix
I tested the fix thoroughly:
Manual testing:
Verified digest preview at
http://localhost:9991/digest/displayed correctlyChecked both HTML and plain text versions
Tested with various topic names, including edge cases
Automated testing:
./tools/lint --skip=gitlint zerver/lib/email_notifications.py
./tools/test-backend zerver/tests/test_email_notifications.py
./tools/run-mypy
I also added a specific test case test_empty_topic_display_in_digest_email to ensure this regression wouldn't happen again.
Edge cases verified:
Empty topic names
Topics with special characters
Topics with potential HTML content
8. Submitting the Pull Request
I submitted PR #37059 with a clear description following Zulip's guidelines:
Explained the problem and solution
Included testing steps
Referenced the original issue #36689
The PR went through Zulip's automated checks:
Linting passed
Type checking passed
All tests passed
Security review flagged an initial XSS concern
I quickly addressed the security feedback with a second commit, properly escaping the user input before wrapping it in Markup().
Pull Request Link: https://github.com/zulip/zulip/pull/37059
9. What I Learned
Technical Learnings:
How Django's
Markupclass works and its security implicationsThe importance of consistent type annotations in large codebases
Zulip's comprehensive testing approach and tooling
The difference between auto-escaping and manual escaping in templates
Non-Technical Learnings:
How to read and understand large, well-organised codebases
The value of thorough local testing before submitting
How security-conscious open source projects operate
The importance of clear, focused commits and PR descriptions
The most valuable lesson was understanding that good open source contributions aren't just about fixing bugs – they're about maintaining code quality, security, and consistency across the entire project.
10. Final Thoughts
This contribution gave me confidence that I can meaningfully contribute to large, complex projects. The Zulip community's focus on mentoring new contributors and maintaining high standards created an excellent learning environment.
I'm excited to continue contributing to Zulip, potentially tackling more complex issues as I become more familiar with the codebase. The experience reinforced that open source contribution is as much about learning and collaboration as it is about coding.
For anyone considering their first Zulip contribution: the documentation is excellent, the community is welcoming, and the codebase is well-organised. Don't hesitate to dive in!
11. Resources
Zulip Repository: https://github.com/zulip/zulip
Original Issue: https://github.com/zulip/zulip/issues/36689
My Pull Request: https://github.com/zulip/zulip/pull/37059
Zulip Development Documentation: https://zulip.readthedocs.io/en/latest/development/overview.html
Zulip Contributing Guide: https://github.com/zulip/zulip/blob/main/CONTRIBUTING.md




