[Bug] Registration verification email never sent in Production — InitiatePreRegistration hits the NoOpEmailService API edge stub #225

Closed
opened 2026-06-15 21:28:38 +00:00 by spikerj · 1 comment
Owner

Summary

In Production, users who start registration never receive their email-verification code. The pre-registration record is created and the API returns Success = true / EmailSent = true, but no email is actually sent — so the user is stuck (they can't verify their email, and SMS verification is gated behind email verification).

Evidence (SEQ)

A Warning is logged on every registration attempt:

Email send invoked on the API edge stub (method=SendEmailAsync, to=chhorton.80@gmail.com, subject=Verify Your Email - SpikerSoft Registration). Skipping outbound SMTP. Refactor the caller to publish SendEmailCommand : IRemoteCommand to SpikerSoft.EventHandlers.Notifications instead of resolving IEmailService directly.

Key log properties:

Field Value
Environment Production
ServiceName SpikerSoft.Api
SourceContext SpikerSoft.Business.Edge.Services.NoOpEmailService
ActionName SpikerSoft.Api.Domain.Authentication.AuthenticationController.InitiatePreRegistration
RequestType InitiatePreRegistrationCommand
Path /api/authentication/initiate-pre-registration
Method SendEmailAsync

Root cause

During the API-edge slimming work, IEmailService on the API is bound to SpikerSoft.Business.Edge.Services.NoOpEmailService — a stub that logs a warning and drops the send (keeps MailKit out of the API publish closure). The real MailKit-backed EmailService runs only inside the SpikerSoft.EventHandlers.Notifications worker host.

InitiatePreRegistrationCommandHandler (in SpikerSoft.Business) still resolves IEmailService directly and calls SendEmailAsync(...) from its SendEmailVerification(...) helper (used by both the initial send and the resend path). On the API that call no-ops, so the verification email is silently dropped. The handler's try/catch then reports EmailSent = true because the no-op "succeeds", masking the failure.

The NoOpEmailService XML doc spells out the intended fix: callers should publish SendEmailCommand : IRemoteCommand onto the notifications exchange so the Notifications worker (which has the real EmailService) sends it.

Impact

  • Severity: High. New-user self-registration via the pre-registration flow is effectively broken in Production — no verification email ⇒ no account.
  • Misleading EmailSent = true in the API response and Success/Serilog signal, so the failure is invisible to the client and to dashboards that key off EmailSent.

Proposed fix

  1. Add a generic SendEmailCommand : IRemoteCommand to SpikerSoft.Contracts.Workers (exchange notifications, routing key notifications.email.send.requested) carrying ToEmail / Subject / BodyHtml / BodyText.
  2. Add a SendEmailCommandHandler in SpikerSoft.Workers.Notifications that resolves the real IEmailService (MailKit EmailService) and sends the message.
  3. Register the new route in NotificationsRpcConsumerHostedService (fire-and-forget / no reply expected).
  4. Refactor InitiatePreRegistrationCommandHandler.SendEmailVerification(...) to publish SendEmailCommand via MediatR (which RemoteDispatchBehavior publishes to RabbitMQ) instead of resolving IEmailService directly. This fixes both the initial-send and resend-email paths.

Acceptance criteria

  • Starting pre-registration in Production results in a verification email actually being delivered (sent by the Notifications worker).
  • No more NoOpEmailService "Skipping outbound SMTP" warning on the InitiatePreRegistration path.
  • EmailSent reflects whether the send command was successfully dispatched.
  • Unit coverage for the new worker handler + the refactored caller; existing pre-registration tests still pass.

Out of scope / follow-ups

The same NoOpEmailService warning will still fire for the other in-process callers listed in its XML doc (RegisterUser, CreateOrganization, ResendPreRegistrationCodes, ParentalEmailService). Those should be migrated to SendEmailCommand in follow-up tickets; this ticket is scoped to the reported registration (pre-registration) flow.

## Summary In **Production**, users who start registration never receive their email-verification code. The pre-registration record is created and the API returns `Success = true` / `EmailSent = true`, but **no email is actually sent** — so the user is stuck (they can't verify their email, and SMS verification is gated behind email verification). ## Evidence (SEQ) A `Warning` is logged on every registration attempt: > Email send invoked on the API edge stub (method=`SendEmailAsync`, to=`chhorton.80@gmail.com`, subject=`Verify Your Email - SpikerSoft Registration`). Skipping outbound SMTP. Refactor the caller to publish `SendEmailCommand : IRemoteCommand` to `SpikerSoft.EventHandlers.Notifications` instead of resolving `IEmailService` directly. Key log properties: | Field | Value | |---|---| | `Environment` | Production | | `ServiceName` | SpikerSoft.Api | | `SourceContext` | `SpikerSoft.Business.Edge.Services.NoOpEmailService` | | `ActionName` | `SpikerSoft.Api.Domain.Authentication.AuthenticationController.InitiatePreRegistration` | | `RequestType` | `InitiatePreRegistrationCommand` | | `Path` | `/api/authentication/initiate-pre-registration` | | `Method` | `SendEmailAsync` | ## Root cause During the API-edge slimming work, `IEmailService` on the API is bound to `SpikerSoft.Business.Edge.Services.NoOpEmailService` — a stub that **logs a warning and drops the send** (keeps MailKit out of the API publish closure). The real MailKit-backed `EmailService` runs only inside the `SpikerSoft.EventHandlers.Notifications` worker host. `InitiatePreRegistrationCommandHandler` (in `SpikerSoft.Business`) still resolves `IEmailService` directly and calls `SendEmailAsync(...)` from its `SendEmailVerification(...)` helper (used by both the initial send and the resend path). On the API that call no-ops, so the verification email is silently dropped. The handler's `try/catch` then reports `EmailSent = true` because the no-op "succeeds", masking the failure. The `NoOpEmailService` XML doc spells out the intended fix: callers should **publish `SendEmailCommand : IRemoteCommand`** onto the `notifications` exchange so the Notifications worker (which has the real `EmailService`) sends it. ## Impact - **Severity: High.** New-user self-registration via the pre-registration flow is effectively broken in Production — no verification email ⇒ no account. - Misleading `EmailSent = true` in the API response and `Success`/Serilog signal, so the failure is invisible to the client and to dashboards that key off `EmailSent`. ## Proposed fix 1. Add a generic `SendEmailCommand : IRemoteCommand` to `SpikerSoft.Contracts.Workers` (exchange `notifications`, routing key `notifications.email.send.requested`) carrying `ToEmail` / `Subject` / `BodyHtml` / `BodyText`. 2. Add a `SendEmailCommandHandler` in `SpikerSoft.Workers.Notifications` that resolves the real `IEmailService` (MailKit `EmailService`) and sends the message. 3. Register the new route in `NotificationsRpcConsumerHostedService` (fire-and-forget / no reply expected). 4. Refactor `InitiatePreRegistrationCommandHandler.SendEmailVerification(...)` to publish `SendEmailCommand` via MediatR (which `RemoteDispatchBehavior` publishes to RabbitMQ) instead of resolving `IEmailService` directly. This fixes both the initial-send and resend-email paths. ## Acceptance criteria - [ ] Starting pre-registration in Production results in a verification email actually being delivered (sent by the Notifications worker). - [ ] No more `NoOpEmailService` "Skipping outbound SMTP" warning on the `InitiatePreRegistration` path. - [ ] `EmailSent` reflects whether the send command was successfully dispatched. - [ ] Unit coverage for the new worker handler + the refactored caller; existing pre-registration tests still pass. ## Out of scope / follow-ups The same `NoOpEmailService` warning will still fire for the other in-process callers listed in its XML doc (`RegisterUser`, `CreateOrganization`, `ResendPreRegistrationCodes`, `ParentalEmailService`). Those should be migrated to `SendEmailCommand` in follow-up tickets; this ticket is scoped to the reported registration (pre-registration) flow.
Author
Owner

Resolved in spikersoft-backend PR #8 (merged to master at 2026-06-15T23:14:29Z).

InitiatePreRegistrationCommandHandler no longer resolves IEmailService directly (the API-edge NoOpEmailService silently dropped the SMTP send, so verification emails never went out while the handler still reported EmailSent = true). It now publishes SendEmailCommand : IRemoteCommand to the notifications exchange; the new SendEmailCommandHandler in SpikerSoft.Workers.Notifications performs the real MailKit send, and NotificationsRpcConsumerHostedService gained fire-and-forget routing for it. Covers both the initial-send and resend-email paths.

Closing.

Resolved in spikersoft-backend PR #8 (merged to `master` at 2026-06-15T23:14:29Z). `InitiatePreRegistrationCommandHandler` no longer resolves `IEmailService` directly (the API-edge `NoOpEmailService` silently dropped the SMTP send, so verification emails never went out while the handler still reported `EmailSent = true`). It now publishes `SendEmailCommand : IRemoteCommand` to the `notifications` exchange; the new `SendEmailCommandHandler` in `SpikerSoft.Workers.Notifications` performs the real MailKit send, and `NotificationsRpcConsumerHostedService` gained fire-and-forget routing for it. Covers both the initial-send and resend-email paths. Closing.
Sign in to join this conversation.