You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(deleter): email the user after their account is deleted - #2034
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.goTestDeleteUser: 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.goTestConnectHandler_DeleteUser: an id that is not a UUID returns INVALID_ARGUMENT.
config/config_test.goTestLoadDeletion: 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.
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.
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: 2
Check name
Status
Explanation
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
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.
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.
Sources consulted:
- mail/smtp.go at v2.3.1 · go-mail/mail · GitHub — https://github.com/go-mail/mail/blob/v2.3.1/smtp.go
- mail/send.go at v2.3.1 · go-mail/mail · GitHub — https://github.com/go-mail/mail/blob/v2.3.1/send.go
- Releases · go-mail/mail · GitHub — https://github.com/go-mail/mail/releases
- mail package - github.com/go-mail/mail - Go Packages — https://pkg.go.dev/github.com/go-mail/mail%40v2.3.1%2Bincompatible
- …and 13 more
Apply the SMTP deadline before the greeting.
DeleteUser sends the notice synchronously after deleting the row. gopkg.in/mail.v2 v2.3.1 applies its default 10-second deadline only after smtp.NewClient returns. If the SMTP server accepts the connection but sends no greeting, smtp.NewClient can block without that deadline, and it does not consume the RPC context. The row is already deleted, but the DeleteUser RPC can remain blocked.
Set the connection deadline before creating the SMTP client, or use a context-aware SMTP implementation.
The reason will be displayed to describe this comment to others. Learn more.
@AmanGIT07 The missing deadline is real: in gopkg.in/mail.v2 v2.3.1, Dial reads the server greeting (smtp.go:91) before it sets the deadline (smtp.go:96-98). Every email Frontier sends goes through the same pkg/mailer path: login OTP and magic link, invitations, PAT alerts, and the org delete email. So the fix belongs in pkg/mailer, for all of them, and not in this PR. Tracked in #2039.
| **app.deletion.resources.\<namespace\>.project** | Rule for a project delete. Missing, it stops the delete. | `block` | No |
| **app.deletion.resources.\<namespace\>.organization** | Rule for an organization delete. Missing, it stops the delete. | `allow` | No |
`user_delete_notice` sets the email a user gets after their account is deleted, by the user or by an admin. It is sent whether or not `enabled` is set, and a disabled user gets it too. The subject and body are Go templates. `.User` holds the deleted user's `ID`, `Name`, `Email`, `Title`, `Avatar` and `State`, as in `{{.User.Title}}`. If sending fails, the delete still succeeds.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_noticeconfig. 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:DeleteUserreads the user first, disabled or not. LikegetProject, 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:parseMailTemplateandsendMailhold the template and send code that both emails use.sendUserDeleteNoticelogs a failure and never returns one.core/deleter/config.go:user_delete_noticewith asubjectand abody, both Go templates. It is sent whether or notenabledis set.internal/api/v1beta1connect/user.go: a user id that is not a UUID returnsINVALID_ARGUMENTinstead ofINTERNAL.config/sample.config.yaml,docs/content/docs/reference/configurations.mdx: the new key.Technical Details
DeleteUserandDeleteCurrentUserboth send it, because both run the deleter'sDeleteUser. A disabled user gets it too..UserhasID,Name,Email,Title,AvatarandState.NOT_FOUND. Anything it left behind (policies, PATs, relations) is still cleared on the way, as before.billing.org_delete_notice.Test Plan
core/deleter/service_test.goTestDeleteUser: 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) andTestSendMail(headers; nothing sent when the subject or the body fails to render).TestDeleteOrganizationpasses unchanged on the shared code.internal/api/v1beta1connect/user_test.goTestConnectHandler_DeleteUser: an id that is not a UUID returnsINVALID_ARGUMENT.config/config_test.goTestLoadDeletion: the new key loads from YAML.go vet ./..., the unit tests of the changed packages with-race, andmake lintpass.