Skip to content

Complete the admin email sending flow - #285

Merged
shaply merged 4 commits into
mainfrom
joel/admin-email-flow
Sep 29, 2026
Merged

shaply merged 4 commits into
mainfrom
joel/admin-email-flow

Conversation

@jlouisugbo

@jlouisugbo jlouisugbo commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #99

Checklist:

  • I have mentioned a ticket above
  • My changes meet the acceptance criteria of said ticket
  • I have self-reviewed my changes
  • I have requested a review from the appropriate team member(s)
  • I have fixed all merge conflicts
  • I have ensured CI checks pass

Description


Admins and owners can compose an email from the inbox, choose recipients in the active organization, and see whether the send succeeded or failed. An empty recipient list is rejected instead of mailing the whole organization. While a send is in flight the composer stays open and cannot be submitted again. A failed send keeps the draft so it can be retried.

Changes Made


  • Inbox New + is limited to admins and owners and opens the send-email composer.
  • Recipient lists page through the members API so organizations with more than 100 members are fully selectable.
  • Subject, body, and at least one recipient are required. Recipients outside the active organization are excluded, and a selection with nobody in the organization is rejected.
  • Sending shows a pending state and blocks closing or submitting again. The composer closes and confirms only after a successful response. A failure keeps the subject, body, recipients, and error so the send can be retried.
  • POST /api/emails enforces admin access, validation, and tenant isolation. Unit and end-to-end tests cover those cases, plus member, guest, pending, and owner viewpoints.

Screenshots/Videos


Admin sends successfully

admin-sends-successfully.webm

Failed send, draft stays, then retry

failed-send-retry.webm

Member sees the inbox with no New button

member-no-new.webm

Guest is sent to login

guest-to-login.webm

Pending join request is sent away from the inbox

pending-join.webm

Owner opens the composer

owner-opens-composer.webm

Teammate is listed and a member of another organization is not

teammate-not-other-org.webm

QA Steps

  1. Sign in as an organization admin and open Inbox.
  2. Confirm New + opens the composer. Submit with no recipient selected and confirm the form stays open with a validation message.
  3. Select a member, enter a subject and body, and send. Confirm the button shows Sending..., the dialog stays open while the request is in flight, then the modal closes and an Email sent banner appears.
  4. Force a send failure. Confirm the subject, body, and selected recipient remain, the error is shown, and a second send can succeed.
  5. Sign in as a member who is not an admin and confirm New + is absent. Open /inbox signed out and confirm the login page. Open it with a pending join request and confirm the join-request status page.
  6. Sign in as an owner and confirm the composer opens. Confirm a teammate appears in the recipient picker and a member of another organization does not.
  7. With more than 100 members, confirm a member past the first page can be selected.

Related Tickets

Empty recipient lists no longer fall through to the whole organization, and send success or failure stays visible in the composer.
Keep the admin send rules and the success toast from main.
@jlouisugbo jlouisugbo self-assigned this Sep 24, 2026
@jlouisugbo
jlouisugbo requested a review from shaply September 24, 2026 20:02
@netlify

netlify Bot commented Sep 25, 2026

Copy link
Copy Markdown

❌ Deploy Preview for servicestart failed. Why did it fail? →

Name Link
🔨 Latest commit 61ace38
🔍 Latest deploy log https://app.netlify.com/projects/servicestart/deploys/6ab5725c2deee8000841dbcf

@netlify

netlify Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for servicestart ready!

Name Link
🔨 Latest commit 2ebcbd5
🔍 Latest deploy log https://app.netlify.com/projects/servicestart/deploys/6abb0d860833f80008977a61
😎 Deploy Preview https://deploy-preview-285--servicestart.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@shaply

shaply commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

@greptileai

@greptile-apps

greptile-apps Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Disclaimer: This comment was AI-generated and is not necessarily completely accurate. Please take code comments with a grain of salt.

RetriggerConfidence Score: 3/5

[Medium risk] Completes the admin email sending feature with validation and error handling.

The PR is not ready to merge until authentication precedence, persistence after delivery failure, and the explicit modal-styling requirement are addressed.

Findings

  1. P1 Invalid requests bypass authentication ▶
  2. P1 Failed delivery leaves saved records ▶
  3. P2 Deletion modals violate typography requirements ▶

Summary

The PR adds an admin email composer, paginated recipient selection, tenant-scoped sending, delivery feedback, and tests.

  • Improve authentication-before-validation ordering and coordinate delivery failures with persisted announcements and messages.
  • Update the changed deletion-modal typography and colors to meet the repository’s modal requirement.
  • Checklist areas needing improvement: API status codes, handling failed backend operations after database writes, and modal styling. Review score: 70/100.
Diagram
sequenceDiagram
  participant Admin
  participant Composer
  participant API
  participant EmailService
  participant Provider
  Admin->>Composer: Select recipients and compose
  Composer->>API: POST /api/emails
  API->>EmailService: Send to active-organization members
  EmailService->>Provider: Send email
  Provider-->>EmailService: Delivery result
  EmailService-->>API: Success or failure
  API-->>Composer: Response
  Composer-->>Admin: Confirmation or retained draft
Loading

Reviews (1) · Last reviewed commit: "Merge origin/main into joel/admin-email-..."

Comment thread api/emails.ts
Comment on lines +34 to +40
zValidator("json", sendEmailSchema, (result, c) => {
if (!result.success) {
return c.json(
{ error: result.error.issues[0]?.message ?? "Invalid email" },
400,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Invalid requests bypass authentication

When a signed-out caller submits an empty recipient list or a whitespace-only subject, this callback returns 400 before requireAdmin runs. The request should receive 401 instead; an empty-recipient request previously reached that authentication check.

Comment thread lib/services/EmailService.ts Outdated
Comment on lines +68 to +70
if (!result.success) {
throw new Error("Failed to send email");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed delivery leaves saved records

If the email provider returns success: false, this new throw reaches the announcement and message callers after they have saved their records. Their requests then fail even though the records remain: retrying publication cannot resend an already-published announcement, and retrying message creation risks duplicate work. Delivery failure needs to be handled together with those writes.

Comment on lines +441 to +446
<p className="text-desktop-paragraph-2! leading-5.5! font-bold! font-paragraph! text-grey-text-strong">
Please Confirm Deletion
</p>
}
description={
<p className="!text-mobile-paragraph-2 !leading-[20px] !font-paragraph text-grey-text-weak">
<p className="text-mobile-paragraph-2! leading-5! font-paragraph! text-grey-text-weak">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Deletion modals violate typography requirements

The changed title and description classes use paragraph styles and grey text. The repository requires modal titles to use responsive heading-1 variables, descriptions to use paragraph-1 variables, and both to use white text. The batch-deletion modal repeats this pattern; this requirement must be satisfied before merging.

Rule Used: Modal titles should use the heading-1 CSS variables from src/styles/globals.css for font-size, font-family, and line-height (responsive for desktop and mobile). Modal descriptions should use the paragraph-1 CSS variables from the same file. Bot... (source)

Learned From
GTBitsOfGood/design-system#76

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

emailMembers now threw when Juno reported an unsuccessful send, which
turned announcement publishing and message creation into 500s after
their rows were already saved. Return a result (sent, no-recipients,
delivery-failed) instead, and let POST /api/emails map it to 400/500.
Other callers keep their previous behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeYmbN1aRFTdeZM8vU2cC3

shaply commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Pushed c055fa8 for Greptile's P1 "Failed delivery leaves saved records". EmailService.emailMembers threw on an unsuccessful Juno result, so announcement publishing and message creation would have returned 500 after their rows were already saved. It now returns "sent" | "no-recipients" | "delivery-failed". POST /api/emails maps those to 200/400/500 (the same statuses as before), and the other callers keep their previous behavior. Added a service test for the failed-delivery case.

Left as-is:

  • "Invalid requests bypass authentication": validating before requireAdmin matches every other route in api/, and a 400 for a malformed body reveals nothing.
  • Modal typography (P2): that rule comes from the design-system repo. White text on these white dialogs would be unreadable.

Generated by Claude Code

@shaply
shaply merged commit a25148a into main Sep 29, 2026
25 checks passed

This branch was successfully deployed

1 active deployment
Preview — 2ebcbd53 Deployed Sep 29, 2026 by shaply via drop-db #257
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.

Complete the admin email sending flow

3 participants