Skip to content

feat(deleter): email the user after their account is deleted - #2034

Merged
AmanGIT07 merged 4 commits into
mainfrom
feat/user-delete-email
Nov 3, 2026
Merged

AmanGIT07 merged 4 commits into
mainfrom
feat/user-delete-email

Conversation

@AmanGIT07

@AmanGIT07 AmanGIT07 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A user delete, by an admin or by the user, now emails the deleted user once the user row is gone. The subject and body come from the new app.deletion.user_delete_notice config. When it is empty, a plain built-in text is used. The org delete email now uses the same template code and is otherwise unchanged.

Changes

  • core/deleter/service.go: DeleteUser reads the user first, disabled or not. Like getProject, it refuses an id that is not a UUID. A failed read stops the delete before anything changes. A user with no row still goes through the cleanup, and the row delete returns not found. After the row delete, it sends the email.
  • core/deleter/delete_notice.go: parseMailTemplate and sendMail hold the template and send code that both emails use. sendUserDeleteNotice logs a failure and never returns one.
  • core/deleter/config.go: user_delete_notice with a subject and a body, both Go templates. It is sent whether or not enabled is set.
  • internal/api/v1beta1connect/user.go: a user id that is not a UUID returns INVALID_ARGUMENT instead of INTERNAL.
  • config/sample.config.yaml, docs/content/docs/reference/configurations.mdx: the new key.

Technical Details

  • The email goes out only after the user row is deleted. A failed step stops the delete before the row, so no email goes out and a retry can still send it. A failed send is logged, and the delete still succeeds.
  • Like the org email, it is sent before the RPC returns.
  • The admin DeleteUser and DeleteCurrentUser both send it, because both run the deleter's DeleteUser. A disabled user gets it too.
  • In the template, .User has ID, Name, Email, Title, Avatar and State.
  • An org deleted with the user still sends its own org email when its delete finishes. The user email does not list these orgs, because a retry cannot see the orgs an earlier attempt deleted.
  • A user that does not exist, or is already deleted, still returns NOT_FOUND. Anything it left behind (policies, PATs, relations) is still cleared on the way, as before.
  • The org email keeps its config at billing.org_delete_notice.
  • e2e cannot check the email, because the e2e server runs without a mail server.

Test Plan

  • core/deleter/service_test.go TestDeleteUser: the email goes to the user's address, after the row delete, with the built-in subject and body; a configured template can use the user's fields; a disabled user gets the email; a failed send does not fail the delete; a broken configured template still deletes the user, without an email; a failed read, or an id that is not a UUID, stops the delete before anything changes; a user with no row still has its leftovers cleared and returns not found; no failure case sends an email.
  • core/deleter/delete_notice_test.go: TestParseMailTemplate (fallbacks, configured values, broken templates) and TestSendMail (headers; nothing sent when the subject or the body fails to render). TestDeleteOrganization passes unchanged on the shared code.
  • internal/api/v1beta1connect/user_test.go TestConnectHandler_DeleteUser: an id that is not a UUID returns INVALID_ARGUMENT.
  • config/config_test.go TestLoadDeletion: the new key loads from YAML.
  • go vet ./..., the unit tests of the changed packages with -race, and make lint pass.

parseMailTemplate picks the configured or built-in subject and body and parses them. sendMail fills them in and sends one mail. The org delete email uses both and sends the same message as before.
DeleteUser reads the user first, disabled or not, and refuses an id that is not a UUID. A failed read stops the delete, and a user with no row still runs the cleanup before the row delete returns not found. Once the row is deleted, it sends the email from app.deletion.user_delete_notice, with plain built-in text when it is empty. The delete handler returns INVALID_ARGUMENT for an id that is not a UUID.
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Nov 2, 2026 8:48am UTC

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Summary

Summary by CodeRabbit

  • New Features
    • User account deletion can send a configurable email notice with a personalized subject and message. Notices can use user details, and email delivery failures do not prevent deletion.
  • Bug Fixes
    • Invalid user IDs now consistently return a bad-request response when deletion is requested.
  • Documentation
    • Configuration guidance now covers deletion notice templates, available user details, default content, and delivery behavior.

The change adds configurable email notices for deleted user accounts. Deletion retrieves the user before cleanup and sends the notice after teardown. The Connect handler also maps invalid UUID errors to invalid-argument responses.

Changes: User deletion flow

Layer / File(s) Summary
Configure and render deletion notices
core/deleter/config.go, config/*, core/deleter/delete_notice.go, core/deleter/delete_notice_test.go, docs/content/docs/reference/configurations.mdx
Deletion configuration adds notice subject and body templates. Shared helpers parse, render, and send organization and user notices. Examples, documentation, and tests cover configuration and mail behavior.
Retrieve, delete, and notify
core/deleter/service.go, core/deleter/service_test.go
DeleteUser retrieves the user before cleanup, continues cleanup when the user is missing, deletes the user record after resource cleanup, and sends a notice after deletion. Tests cover lookup and cleanup outcomes, notice delivery, disabled users, and operation ordering.
Map invalid UUID errors
internal/api/v1beta1connect/user.go, internal/api/v1beta1connect/user_test.go
The Connect delete-user handler maps user.ErrInvalidUUID to connect.CodeInvalidArgument. A test covers the invalid UUID response.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Suggested reviewers: whoabhisheksah, rohilsurana

Merge Risk: Low · up to 0b527

When SMTP is not configured, deletion notices are silently undelivered while logs report success; correct the mailer wiring before relying on those logs.

Pre-merge checks: Passed 2
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coveralls

coveralls commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41415687973

Coverage increased (+0.06%) to 56.053%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 4 uncovered changes across 1 file (70 of 74 lines covered, 94.59%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
core/deleter/delete_notice.go 51 47 92.16%
Total (3 files) 74 70 94.59%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
core/deleter/delete_notice.go 1 82.32%

Coverage Stats

Coverage Status
Relevant Lines: 42189
Covered Lines: 23648
Line Coverage: 56.05%
Coverage Strength: 19.7 hits per line

💛 - Coveralls

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 8190c1f and b6c8898.

Files selected for processing: 11
  • config/config_test.go
  • config/sample.config.yaml
  • config/testdata/deletion.yaml
  • core/deleter/config.go
  • core/deleter/delete_notice.go
  • core/deleter/delete_notice_test.go
  • core/deleter/service.go
  • core/deleter/service_test.go
  • docs/content/docs/reference/configurations.mdx
  • internal/api/v1beta1connect/user.go
  • internal/api/v1beta1connect/user_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Comment thread core/deleter/service.go Resolved
Comment thread core/deleter/service_test.go Resolved
Comment thread core/deleter/delete_notice_test.go Resolved

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

Minor · Pass a disabled mailer to the deleter. · delete_notice.go:238-253

core/deleter/delete_notice.go:238-253
Minor · Functional Correctness · Quick win

Pass a disabled mailer to the deleter.

DeleteUser is reachable through the authenticated DeleteUser endpoint. Without app.mailer.smtp_host, the server passes a non-nil mock dialer to the deleter. The mock returns nil from DialAndSend without delivery, so sendUserDeleteNotice logs sent the user delete notice. This violates the documented contract that the notice is sent only when SMTP is configured.

Suggested fix
 var mailDialer mailer.Dialer = mailer.NewMockDialer()
+var deletionMailDialer mailer.Dialer
 if cfg.App.Mailer.SMTPHost != "" && cfg.App.Mailer.SMTPHost != "smtp.example.com" {
 	mailDialer = mailer.NewDialerImpl(cfg.App.Mailer.SMTPHost,
 		cfg.App.Mailer.SMTPPort,
 		cfg.App.Mailer.SMTPUsername,
 		cfg.App.Mailer.SMTPPassword,
 		cfg.App.Mailer.SMTPInsecure,
 		cfg.App.Mailer.Headers,
 		cfg.App.Mailer.TLSPolicy(),
 	)
+	deletionMailDialer = mailDialer
 	logger.Info("mailer enabled", "host", cfg.App.Mailer.SMTPHost, "port", cfg.App.Mailer.SMTPPort)
 }
 
 // ...
-	mailDialer, cfg.Billing.OrgDeleteNotice, cfg.App.Deletion,
+	deletionMailDialer, cfg.Billing.OrgDeleteNotice, cfg.App.Deletion,

Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between b6c8898 and 0b5270d.

Files selected for processing: 3
  • core/deleter/delete_notice_test.go
  • core/deleter/service_test.go
  • docs/content/docs/reference/configurations.mdx

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

@AmanGIT07
AmanGIT07 merged commit e1ef7b2 into main Nov 3, 2026
8 checks passed
@AmanGIT07
AmanGIT07 deleted the feat/user-delete-email branch November 3, 2026 06:16

This branch was successfully deployed

1 active deployment
Preview — 0b5270d0 Deployed Nov 2, 2026 by vercel[bot]
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.

3 participants