Skip to content

Email 5/7 (PR 4): admin Email setup page with a test email #423

Closed

Description

@HMarzban

Part of #418. Read the parent first. Its Email setup page section is the contract here. Step 5 of 7. server/ is short for apps/hocuspocus.server/.

Goal

Give the admin one page that answers "is email set up correctly?" and can send a test email. It needs no learning curve. The page never accepts or shows a secret; .env stays the only source.

Owner and gate

Steps

  1. Module server/src/modules/email-setup/, in the shape of server/src/modules/link-metadata/:

    • domain/toEmailSetupView.ts: toEmailSetupView(delivery, facts, extras): EmailSetupView. Inputs are config.email.delivery and config.email.facts, so no secret string enters the module. It is the only code that builds the page data. It masks the bounce row's address with maskEmail, drops username, and cuts reason to 80 characters.
    • infra/testSendBudget.ts: createTestSendBudget(redis). It wraps new RateLimiterRedis({ storeClient: redis, points: 1, duration: 60, keyPrefix: 'email-test-send' }) and calls consumeWithin (server/src/lib/consumeWithin.ts), as server/src/modules/mcp/infra/toolBudget.ts does. limited, unavailable and no Redis all refuse.
    • http/controller.ts and http/router.ts, using ok and fail from server/src/http/envelope.ts.
    • module.ts: init(deps: InitDeps): InitResult. Deps: delivery, facts, getEmailProvider, deliverEmail, getEmailDlqDepth, getLatestBounce, redis, logger.
  2. Routes, mounted in server/src/api/routers/admin.router.ts with admin.route('/email/setup', emailSetup.init(deps).router). The existing admin.use('*', adminAuthMiddleware) (:31) covers them. The parent explains why this mount is a deliberate exception.

    • GET /api/admin/email/setup returns the view:
      • provider and status, with the problems for invalid;
      • EMAIL_FROM, and set or missing per secret;
      • SMTP_HOST and SMTP_PORT;
      • a live checkConnection, bounded by EMAIL_CHECK_TIMEOUT_MS;
      • the newest get_email_bounces row (server/src/api/controllers/admin-audit.controller.ts:195-199), masked as above;
      • the email DLQ depth.
    • POST /api/admin/email/setup/test-send takes no body. It calls deliverEmail to the signed-in admin's own address, with the key <namespace>/test/<uuid>. It answers with the message id, or the error kind and code.
    • Add getEmailDlqDepth() to server/src/lib/email/queue.ts, using EMAIL_DLQ_NAME.
  3. Page apps/admin-dashboard/src/pages/email.tsx (create). Add one entry labeled Email setup to apps/admin-dashboard/src/constants/navigation.ts. Copy the layout and data-loading pattern of apps/admin-dashboard/src/pages/notifications.tsx, and build from .cursor/docs/design-system.md. The page shows:

    • a status line: ready, off, or invalid, with each problem in plain words;
    • a table of values: set or missing for secrets, and plain values for the rest;
    • the connection check result;
    • the webhook state;
    • the email DLQ depth, with a link to the failure summary;
    • a copy-ready .env block, with a blank line for each missing value and the note "Edit the host env file, then redeploy";
    • one Send test email button, with its result shown inline.

    It also says that the test email runs in rest-api, and that the worker's state is in the failure summary.

  4. Public /api/email/health: reduce it to { status, queue_connected }, following Stop REST /health from leaking error text or taking every /api route down #405's rule that public routes return status only. Point the Notifications page's gateway card (apps/admin-dashboard/src/services/api.ts:476, :493-495, apps/admin-dashboard/src/pages/notifications.tsx:459) at GET /api/admin/email/setup. Update the route in server/src/modules/openapi/domain/paths/email.ts.

Test (one file)

server/src/modules/email-setup/__tests__/unit/toEmailSetupView.test.ts. Feed the projection facts where every secret is set, and assert that each reads set. Feed it facts where every secret is unset, and assert missing. Feed it a bounce row, and assert the masked address, no username, and a reason of at most 80 characters. The input type carries no secret value, so the test checks the projection, not a leak.

Docs (same PR)

  • server/API.md and server/src/modules/openapi/domain/paths/admin.ts: both routes.
  • apps/admin-dashboard/CLAUDE.md: the Email setup page is read-only for config. It never shows a secret value or tail. Credentials are never written from the UI.

Checks before push

  • bun run check at the repo root, and bun test in server/.
  • Import domain/toEmailSetupView.ts with REDIS_HOST, REDIS_PORT and DATABASE_URL unset.

Deploy

The deploy subject is exactly (build): back front. The deploy step rolls every service, admin-dashboard included (.github/workflows/prod.docs.plus.yml:687-692).

Acceptance

  • On staging, the page and the GET /api/admin/email/setup response show every secret only as set or missing, and no unmasked address.
  • Public GET /api/email/health returns only status and queue_connected, and the Notifications gateway card still works.
  • Send test email delivers to the admin's address and shows the message id.
  • A second test email within 60 s is refused.
  • Run locally with SMTP_HOST set and EMAIL_PROVIDER unset. The page shows invalid, names the problem, and shows the .env lines to add.
  • bun test is green, including the new file.
  • Every doc listed above is updated in this PR.

Exit evidence

Post as a comment here:

  • bun run check and bun test output.
  • A screenshot of the page on staging, checked for unmasked addresses before posting.
  • The JSON of GET /api/admin/email/setup.

Revert

git revert the merge, then redeploy. Nothing else depends on these routes.

Activity

  1. added theissue type on Oct 6, 2026
  2. changed the title [-]Email 5/7 (PR 4): admin Email connector page (read-only config, test send)[/-] [+]Email 5/7 (PR 4): admin Email setup page with a test email[/+] on Oct 6, 2026
  3. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Fixed and live.

    • Commits: 6d69afb7f, 9df360006, 08d907e94
    • Deployed in bbdaae66c (production run 37976785981, green).
    • Verified on production: the public email health returns status only. The admin Email setup page is deployed; a browser check of it by the maintainer is still owed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions