Skip to content

feat(backend): send release confirmation emails#6905

Draft
theosanderson-agent wants to merge 11 commits into
mainfrom
agent/release-confirmation-emails
Draft

feat(backend): send release confirmation emails#6905
theosanderson-agent wants to merge 11 commits into
mainfrom
agent/release-confirmation-emails

Conversation

@theosanderson-agent

@theosanderson-agent theosanderson-agent commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

This adds batched release confirmation emails for sequence approvals. Once enabled, the backend checks for newly released sequence versions every minute, groups them by approver and group, emails the approver, and copies the group's contact address. Each message lists the released accession versions grouped by organism and links back to the group's released-sequences pages.

Delivery bookkeeping lives in a new sent_notifications table rather than on sequence_entries. This avoids firing the sequence table's update-tracker trigger when an email is recorded. One row is stored for each sequence version included in a successfully accepted SMTP message, including the To/CC address snapshots and shared message ID. Existing releases are seeded as handled by the migration, preventing an historical email backlog during rollout.

The scheduled worker uses the existing database-backed task lock so only one backend replica processes a batch. Failed recipient lookups or SMTP sends are isolated per approver/group and leave the affected sequences pending for a later retry. Group CC is omitted when it duplicates the approver address.

The backend now includes Spring Mail support and reuses the Helm chart's existing SMTP settings and password secret. Release confirmations are feature-flagged with releaseConfirmationEmails.enabled and remain disabled by default. The default interval is 60 seconds.

Validation

  • ./gradlew test --tests 'org.loculus.backend.service.notification.*' --console=plain
  • ./gradlew ktlintCheck --console=plain
  • helm lint kubernetes/loculus -f kubernetes/loculus/values.yaml
  • Rendered the chart with preview SMTP values and releaseConfirmationEmails.enabled=true, confirming all backend SMTP arguments and the password secret reference.

The full backend suite was also run. It completed 550 tests with 29 failures, all in existing file-upload tests after the test MinIO returned 507 Insufficient Storage or invalid/missing upload results. The host filesystem was at 100% usage with 1.1 GB free. The focused notification tests, including the PostgreSQL migration/persistence test, pass. That integration test also verifies notification inserts do not add a table_update_tracker entry.

Rollout

Configure auth.smtp, ensure the existing smtp-password secret is populated, and set releaseConfirmationEmails.enabled=true. Deploying with the default values performs the schema migration but does not send email.

🚀 Preview: https://agent-release-confirmatio.loculus.org

@claude claude Bot added backend related to the loculus backend component deployment Code changes targetting the deployment infrastructure labels Jul 10, 2026
@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

This PR may be related to: #4317

@theosanderson

Copy link
Copy Markdown
Member

@claude review @codex review

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @theosanderson's task in 2m 52s —— View job


Review complete ✅

  • Gather context (PR description, changed files)
  • Read new notification service/task/DB service code
  • Read migration SQL and schema changes
  • Read Helm chart changes
  • Read tests
  • Post review feedback

Overall this is a well-structured, cleanly-tested feature (good use of the existing TaskLock pattern, sensible batching by approver+group, and a separate sent_notifications table to avoid tripping the table_update_tracker trigger — nice attention to detail, confirmed by the dedicated integration test). A few things worth considering before merge:

1. Send-then-record is not atomic — possible duplicate emails on transient DB failure

ReleaseConfirmationEmailTask.task() (backend/src/main/kotlin/org/loculus/backend/service/notification/ReleaseConfirmationEmailTask.kt#L43-L66) sends the email first, then calls databaseService.markNotificationsAsSent(...). If the SMTP send succeeds but the subsequent DB write throws (connection blip, deadlock, etc.), the catch block logs "the notification will be retried" — but since the rows were never marked sent, getPendingReleaseNotifications() will pick the same sequences up again next run and re-send the email to the approver. The PR description frames retries as safe ("Failed recipient lookups or SMTP sends are isolated... and leave the affected sequences pending for a later retry"), which is true for failures before the send, but not for this specific gap between a successful send and a failed DB write. Given emails are inherently non-transactional this may be an accepted tradeoff, but it's worth calling out explicitly (and maybe worth a comment in the code) since duplicate confirmation emails are a visible user-facing side effect.

2. A single malformed/blank group contact email permanently blocks that group's batch

groupContactEmail (GroupsTable.contactEmailColumn, a plain non-nullable text column) is passed straight into MimeMessageHelper.setCc(ccEmail) in ReleaseConfirmationEmailService.kt#L48-L50 with no format validation. I couldn't find any validation elsewhere in group creation/management enforcing this field is a well-formed email address (it may historically have been treated as free-text). If any existing group has a blank or malformed contact email, that whole approver/group batch will throw on every scheduled run (caught by the broad catch (exception: Exception) in the task), silently retried forever every runEverySeconds, generating repeated error logs and never delivering the confirmation for that group. Might be worth validating/sanitizing at read time, or at least logging distinctly from a transient SMTP failure so it's easy to spot an operator needs to fix group data.

Minor notes

  • getApproverEmail does one Keycloak lookup per (approver, groupId) batch, so an approver with pending releases across multiple groups triggers redundant Keycloak calls in the same task run — not a correctness issue at expected scale, just a minor inefficiency.
  • The email body's closing line ("This message was sent to the user who approved the release and copied to the group's contact email.") is slightly inaccurate when the CC is intentionally omitted because it matches the approver's address — cosmetic only.
  • Verified the linked website route (/{organism}/submission/{groupId}/released) matches SubmissionRouteUtils.toUrl in website/src/routes/SubmissionRoute.ts, so the email link construction is correct.
  • Verified the new FK (sent_notifications.accession, version → sequence_entries.accession, version with ON DELETE CASCADE) and the migration backfill logic look consistent with the existing schema.

Nothing above is blocking — the core design (batching, task locking, feature flag defaulting to off, backfill seeding to avoid a historical backlog) is sound and the tests cover the main happy/failure paths well. Items 1 and 2 are the ones I'd most want a decision on before enabling this in production.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac359f73fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

helper.setFrom(from)
helper.setTo(recipientEmail)
if (!recipientEmail.equals(ccEmail, ignoreCase = true)) {
helper.setCc(ccEmail)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not let an invalid group CC block confirmations

This makes the group contact address a required SMTP recipient for every confirmation. If a group has an invalid or stale contactEmail (the backend stores it as an unconstrained string), setCc/SMTP delivery can fail or partially send, and the surrounding catch leaves the whole approver/group batch pending, so the valid approver either never gets the confirmation or gets duplicate retries. Validate/skip the CC independently or avoid letting CC failures block recording the To delivery.

Useful? React with 👍 / 👎.

@theosanderson theosanderson added the preview Triggers a deployment to argocd label Jul 10, 2026
theosanderson and others added 7 commits July 10, 2026 23:02
…ging sent ones

Replace the sent_notifications table with a pending_release_notifications
queue that batches release confirmation emails per group, validates
configured from/reply-to addresses, and summarizes accessions per organism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop organism, approver, and group_id from pending_release_notifications;
they are immutable per accession-version and already stored on
sequence_entries, so read them back via a join keyed on (accession,
version). The FK's ON DELETE CASCADE guarantees the join always resolves.
Simplifies enqueue to take only the released accession-versions and
replaces the covering index with one on enqueued_at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The queue now derives group_id via a join, so approval no longer needs to
group released accession-versions by group before enqueueing. Revert the
release update to the flat accession-version list it used before the
notification feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The task already sorts organisms and sorts+caps each organism's accessions
when building the notification content, so drop the redundant re-sort,
re-cap, and duplicate MAX_ACCESSIONS constant in the email body renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Classify each released accession-version as a new submission (version 1),
a revision (version > 1), or a revocation (is_revocation), reading
is_revocation back from sequence_entries via the existing join. The email
now labels each revision/revocation line and adds a per-kind breakdown
("This included 2 new, 1 revised.") when anything is not a plain new
submission; the all-new case stays unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fold approver and groupId into ReleaseNotificationContent instead of
threading them as separate arguments through sendReleaseConfirmation,
populateMessage, and buildBody. Extract the two duplicated single-address
parsers into one shared parseSingleInternetAddress helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend related to the loculus backend component deployment Code changes targetting the deployment infrastructure preview Triggers a deployment to argocd update_db_schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants