Skip to content

feat(messages_v2): add Messages V2 for emailing program offerers/hosts - #1000

Draft
japsu wants to merge 4 commits into
mainfrom
feature/messages-v2
Draft

feat(messages_v2): add Messages V2 for emailing program offerers/hosts#1000
japsu wants to merge 4 commits into
mainfrom
feature/messages-v2

Conversation

@japsu

@japsu japsu commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Adds a new messages_v2 Django app and Program V2 admin UI for composing
and sending Markdown messages to program offerers/hosts, selected via
OR-of-AND involvement dimension filters. Messages have a
draft -> active (sent) -> optionally expired lifecycle; edits to an
already-sent message are not retroactive. New involvements matching an
active message's filters are sent to automatically. Recipients can view
messages they've received on their profile.

Backend:

  • Message (UUID7 PK, created_at derived from it), MessageReplyTo,
    MessageBody (dedup store), MessageRecipient models
  • Security-critical rendering pipeline: placeholders are tokenized,
    substituted after Markdown rendering and nh3 sanitization, and
    HTML-escaped, so recipient-controlled values can never inject
    Markdown or HTML
  • Celery send task + auto-send hook on Involvement.refresh_dependents()
  • GraphQL queries/mutations under program_v2 CBAC, event log entries
  • Two integration tests covering compose->send (incl. injection
    payloads) and auto-send + non-retroactive edit

Frontend:

  • New program-messages admin routes (list, new, edit) sharing a
    MessageComposeCard; recipient filters edited via a modal whose
    selection state survives the modal's mount/unmount cycle
    (RecipientFilterField)
  • New MarkdownText SchemaForm field type backed by
    @uiw/react-md-editor, toolbar restricted to formatting the backend
    actually allows
  • Reply-to address management on the program preferences page
  • Profile "received messages" view
  • Full en/fi/sv translations

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

japsu and others added 4 commits July 28, 2026 20:37
Adds a new messages_v2 Django app and Program V2 admin UI for composing
and sending Markdown messages to program offerers/hosts, selected via
OR-of-AND involvement dimension filters. Messages have a
draft -> active (sent) -> optionally expired lifecycle; edits to an
already-sent message are not retroactive. New involvements matching an
active message's filters are sent to automatically. Recipients can view
messages they've received on their profile.

Backend:
- Message (UUID7 PK, created_at derived from it), MessageReplyTo,
  MessageBody (dedup store), MessageRecipient models
- Security-critical rendering pipeline: placeholders are tokenized,
  substituted after Markdown rendering and nh3 sanitization, and
  HTML-escaped, so recipient-controlled values can never inject
  Markdown or HTML
- Celery send task + auto-send hook on Involvement.refresh_dependents()
- GraphQL queries/mutations under program_v2 CBAC, event log entries
- Two integration tests covering compose->send (incl. injection
  payloads) and auto-send + non-retroactive edit

Frontend:
- New program-messages admin routes (list, new, edit) sharing a
  MessageComposeCard; recipient filters edited via a modal whose
  selection state survives the modal's mount/unmount cycle
  (RecipientFilterField)
- New MarkdownText SchemaForm field type backed by
  @uiw/react-md-editor, toolbar restricted to formatting the backend
  actually allows
- Reply-to address management on the program preferences page
- Profile "received messages" view
- Full en/fi/sv translations

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Address findings from a security/correctness/performance review of Messages V2:

- Only record a MessageRecipient once the email actually sends (fail_silently
  off + logged), so a transient failure no longer permanently marks a person as
  delivered and blocks retries.
- Refuse to send a message with empty recipient filters (an empty AND-group
  would otherwise match every involvement in the universe); drafts may still be
  saved empty.
- Validate reply-to email addresses in the create/update mutations, which
  bypassed EmailField validation.
- Skip the auto-send Celery dispatch unless the universe has an active message,
  avoiding a task per involvement change system-wide.
- Resolve recipients with a single OR'd queryset instead of a Python id set.
- Extract group_to_dimension_filters() shared by message.py and tasks.py.
- Make MessageBody.digest unique so dedup get_or_create is race-safe.

Frontend:
- Real Swedish translation for the profile messages description.
- Join recipient filter groups with language-neutral " / " instead of " OR ".
- Localize the MarkdownEditor "insert heading" aria-label.
- Parse the recipientFilters hidden field via a helper that fails cleanly on
  invalid JSON instead of raising an opaque 500.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant