Add consistent email footer across all templates#891
Conversation
Every email now gets a unified footer: a configurable sign-off
(defaulting to "Regards, The Thesis Management Team.") followed by a
notification settings link appended centrally by MailBuilder.send().
- Add emailSignature field to ResearchGroupSettings for per-group
customization
- Set default emailSignature template variable in MailBuilder constructor
- Append notification settings link centrally in MailBuilder.send()
after Thymeleaf rendering
- Replace hardcoded "Best regards" sign-offs and broken
${config.signature} references with ${emailSignature} in all templates
- Remove per-template notification link blocks (now handled centrally)
- Add Liquibase migration 36 for schema + existing template data updates
- Add assertEmailFooter e2e helper and footer assertions in 6 test specs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WalkthroughE2E tests across email workflows now validate footer presence via a new ChangesEmail footer testing and signature infrastructure
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
server/src/main/resources/db/changelog/changes/28_data_export.sql (1)
14-32:⚠️ Potential issue | 🟠 MajorModification of existing changeset; also ON CONFLICT prevents updates.
Two concerns:
- Modifying changeset 28 after it's been applied will cause Liquibase checksum failures
- The
ON CONFLICT DO NOTHINGclause means existingDATA_EXPORT_READYtemplates won't receive theemailSignatureplaceholder update anywayEnsure changeset 36 handles the UPDATE for existing rows. The modification here only affects fresh deployments.
As per coding guidelines: "Never modify existing changesets that have been applied; create new ones."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/main/resources/db/changelog/changes/28_data_export.sql` around lines 14 - 32, Do not modify changeset 28; revert that file to its original INSERT-only content and create a new changeset (e.g., changeset 36) that performs an UPDATE on email_templates for template_case = 'DATA_EXPORT_READY' and language = 'en' (and research_group_id IS NULL) to append or inject the <div th:utext="${emailSignature}"></div> into body_html so existing rows get the placeholder; ensure the new changeset uses an UPDATE statement (not INSERT ... ON CONFLICT DO NOTHING) targeting the email_templates rows and include a clear id/author so Liquibase checksum history remains intact.server/src/main/resources/db/changelog/changes/19_add_interview_invitation_email_template.sql (1)
1-135:⚠️ Potential issue | 🟠 MajorModification of existing changeset will cause checksum failures.
Same issue as changeset 14 — this changeset (19) has likely been applied to deployed databases. Modifying the template bodies at lines 26, 55, 103, and 133 will break Liquibase checksum validation.
The migration should be handled by a new changeset (e.g., changeset 36) that UPDATEs existing rows rather than modifying historical INSERTs.
As per coding guidelines: "Never modify existing changesets that have been applied; create new ones."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/main/resources/db/changelog/changes/19_add_interview_invitation_email_template.sql` around lines 1 - 135, You changed an already-applied Liquibase changeset (changeset 19) which will break checksums; instead create a new changeset (e.g., changeset 36) that issues UPDATE statements to modify the body_html/subject/description for the existing template rows. Locate the templates by their template_case values ('INTERVIEW_INVITATION', 'INTERVIEW_INVITATION_REMINDER', 'INTERVIEW_SLOT_BOOKED_CONFORMATION', 'INTERVIEW_SLOT_BOOKED_CANCELLATION') and write UPDATE ... SET body_html = '...', subject = '...', description = '...' WHERE template_case = '...'; add this new SQL changeset file and do not alter the original INSERTs in changeset 19.
🧹 Nitpick comments (3)
client/e2e/application-workflow.spec.ts (1)
146-161: Consider asserting sender/footer for the supervisor notification too.For consistency with the other recipient checks in this test, add
assertSentFromAppandassertEmailFooterforsupervisorChairEmail.♻️ Suggested change
const supervisorChairEmail = findBySubject(supervisorEmails, 'New Thesis Application') expect( supervisorChairEmail, 'Supervisor (supervisor@test.local) should receive "New Thesis Application" email', ).toBeDefined() + assertSentFromApp(supervisorChairEmail!) + assertEmailFooter(supervisorChairEmail!)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@client/e2e/application-workflow.spec.ts` around lines 146 - 161, Add assertions to verify the supervisor notification's sender and footer like you do for the examiner: after locating supervisorChairEmail (from waitForNewMessages and findBySubject) call assertSentFromApp(supervisorChairEmail) and assertEmailFooter(supervisorChairEmail). This will mirror the existing checks on examinerChairEmail and ensure the supervisor email's sender and footer are validated as well.server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java (1)
64-75:@Autowiredis redundant on single-constructor classes.Spring Boot auto-wires the sole constructor without the annotation.
♻️ Optional: Remove redundant annotation
- `@Autowired` public MailingService( JavaMailSender javaMailSender, UploadService uploadService,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java` around lines 64 - 75, The `@Autowired` annotation on the MailingService constructor is redundant because Spring will auto-wire the single constructor; remove the `@Autowired` annotation from the MailingService constructor (the constructor that takes JavaMailSender, UploadService, MailConfig, EmailTemplateRepository) so the class relies on constructor injection without the unnecessary annotation.server/src/main/java/de/tum/cit/aet/thesis/utility/MailBuilder.java (1)
590-595: Consider extracting the hardcoded footer HTML.The notification settings footer is inlined here, making future changes error-prone if the same link/text is referenced elsewhere.
♻️ Extract to a constant
+ private static final String NOTIFICATION_FOOTER_TEMPLATE = + "<hr/><div style=\"text-align: center; font-size: 10px;\">" + + "You can (un)subscribe to similar emails in your " + + "<a href=\"%s/settings/notifications\">notification settings in Thesis Management</a>.</div>"; + public void send(JavaMailSender mailSender, UploadService uploadService) { // ... existing code ... String renderedHtml = config.getTemplateEngine().process(templateHtml, templateContext); - renderedHtml += "<hr/><div style=\"text-align: center; font-size: 10px;\">" - + "You can (un)subscribe to similar emails in your " - + "<a href=\"" + config.getClientHost() + "/settings/notifications\">" - + "notification settings in Thesis Management</a>.</div>"; + renderedHtml += String.format(NOTIFICATION_FOOTER_TEMPLATE, config.getClientHost()); messageBody.setContent(renderedHtml, "text/html; charset=utf-8");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/main/java/de/tum/cit/aet/thesis/utility/MailBuilder.java` around lines 590 - 595, The hardcoded notification-settings footer is inlined when building renderedHtml in MailBuilder (where templateHtml, renderedHtml and messageBody are used); extract that HTML snippet into a single reusable private static final String (e.g. NOTIFICATION_SETTINGS_FOOTER) in the MailBuilder class and replace the inline concatenation ("<hr/><div...<a href=\"" + config.getClientHost() + "/settings/notifications\">...") with appending that constant (compose the href using config.getClientHost() if it must be dynamic or store a format string constant and String.format it), keeping the same "text/html; charset=utf-8" content type on messageBody.setContent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java`:
- Around line 83-84: Sanitize the email signature before saving: in
ResearchGroupSettingsController where you set toSave.setEmailSignature(...),
trim and null-check the incoming newSettings.emailSettings().emailSignature(),
then call HtmlUtils.htmlEscape(...) on the trimmed value and persist that
escaped string (or null if empty) so stored signatures cannot inject raw HTML
into outbound templates; follow the same pattern used for
applicantName/thesisTitle in MailingService.
In
`@server/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.java`:
- Around line 5-11: Add the required JSON include annotation to the DTO by
annotating the ResearchGroupSettingsEmailDTO record with
`@JsonInclude`(JsonInclude.Include.NON_EMPTY) (importing
com.fasterxml.jackson.annotation.JsonInclude) so that null/empty fields like
applicationNotificationEmail and emailSignature are omitted during
serialization; ensure the annotation is placed on the record declaration (above
ResearchGroupSettingsEmailDTO) and verify imports are updated, leaving the
fromEntity(...) factory method unchanged.
In
`@server/src/main/resources/db/changelog/changes/36_consistent_email_footer.sql`:
- Around line 40-44: The changeset
consistent-email-footer:36-add-signature-to-templates-without is too broad
because it updates every row in email_templates whose body_html lacks
emailSignature or "Best regards"; narrow the scope by adding explicit
template_case filtering so only templates that should have a signature are
modified (e.g., add AND template_case IN (...) or AND template_case NOT IN (...)
to the WHERE clause), referencing the email_templates table and body_html column
to locate the UPDATE and adjust the WHERE to target intended template types
rather than relying solely on absence detection.
---
Outside diff comments:
In
`@server/src/main/resources/db/changelog/changes/19_add_interview_invitation_email_template.sql`:
- Around line 1-135: You changed an already-applied Liquibase changeset
(changeset 19) which will break checksums; instead create a new changeset (e.g.,
changeset 36) that issues UPDATE statements to modify the
body_html/subject/description for the existing template rows. Locate the
templates by their template_case values ('INTERVIEW_INVITATION',
'INTERVIEW_INVITATION_REMINDER', 'INTERVIEW_SLOT_BOOKED_CONFORMATION',
'INTERVIEW_SLOT_BOOKED_CANCELLATION') and write UPDATE ... SET body_html =
'...', subject = '...', description = '...' WHERE template_case = '...'; add
this new SQL changeset file and do not alter the original INSERTs in changeset
19.
In `@server/src/main/resources/db/changelog/changes/28_data_export.sql`:
- Around line 14-32: Do not modify changeset 28; revert that file to its
original INSERT-only content and create a new changeset (e.g., changeset 36)
that performs an UPDATE on email_templates for template_case =
'DATA_EXPORT_READY' and language = 'en' (and research_group_id IS NULL) to
append or inject the <div th:utext="${emailSignature}"></div> into body_html so
existing rows get the placeholder; ensure the new changeset uses an UPDATE
statement (not INSERT ... ON CONFLICT DO NOTHING) targeting the email_templates
rows and include a clear id/author so Liquibase checksum history remains intact.
---
Nitpick comments:
In `@client/e2e/application-workflow.spec.ts`:
- Around line 146-161: Add assertions to verify the supervisor notification's
sender and footer like you do for the examiner: after locating
supervisorChairEmail (from waitForNewMessages and findBySubject) call
assertSentFromApp(supervisorChairEmail) and
assertEmailFooter(supervisorChairEmail). This will mirror the existing checks on
examinerChairEmail and ensure the supervisor email's sender and footer are
validated as well.
In `@server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java`:
- Around line 64-75: The `@Autowired` annotation on the MailingService constructor
is redundant because Spring will auto-wire the single constructor; remove the
`@Autowired` annotation from the MailingService constructor (the constructor that
takes JavaMailSender, UploadService, MailConfig, EmailTemplateRepository) so the
class relies on constructor injection without the unnecessary annotation.
In `@server/src/main/java/de/tum/cit/aet/thesis/utility/MailBuilder.java`:
- Around line 590-595: The hardcoded notification-settings footer is inlined
when building renderedHtml in MailBuilder (where templateHtml, renderedHtml and
messageBody are used); extract that HTML snippet into a single reusable private
static final String (e.g. NOTIFICATION_SETTINGS_FOOTER) in the MailBuilder class
and replace the inline concatenation ("<hr/><div...<a href=\"" +
config.getClientHost() + "/settings/notifications\">...") with appending that
constant (compose the href using config.getClientHost() if it must be dynamic or
store a format string constant and String.format it), keeping the same
"text/html; charset=utf-8" content type on messageBody.setContent.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
client/e2e/application-review-workflow.spec.tsclient/e2e/application-workflow.spec.tsclient/e2e/mailpit.tsclient/e2e/presentation-workflow.spec.tsclient/e2e/proposal-feedback-workflow.spec.tsclient/e2e/thesis-grading-workflow.spec.tsclient/e2e/thesis-workflow.spec.tsclient/src/requests/responses/researchGroupSettings.tsserver/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.javaserver/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.javaserver/src/main/java/de/tum/cit/aet/thesis/entity/ResearchGroupSettings.javaserver/src/main/java/de/tum/cit/aet/thesis/service/MailingService.javaserver/src/main/java/de/tum/cit/aet/thesis/utility/MailBuilder.javaserver/src/main/resources/db/changelog/changes/14_add_automatic_rejection_reminder_email_template.sqlserver/src/main/resources/db/changelog/changes/19_add_interview_invitation_email_template.sqlserver/src/main/resources/db/changelog/changes/28_data_export.sqlserver/src/main/resources/db/changelog/changes/36_consistent_email_footer.sqlserver/src/main/resources/db/changelog/db.changelog-master.xmlserver/src/main/resources/db/changelog/manual/insert_tum_aet_email_templates.sqlserver/src/main/resources/db/changelog/manual/insert_tum_default_email_templates.sql
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche Nice approach — centralizing the footer in MailBuilder.send() and making the signature configurable per research group is clean. Migration 36 handles deployed databases correctly. However, the modifications to past Liquibase migrations (14, 19, 28) will break checksum validation on any database that already applied them. See inline comment.
- Renumber the consistent-email-footer migration to 40 (after develop's 36-39)
and update its changeset IDs accordingly.
- Resolve the seed-template SQL conflicts in favour of develop's role-naming
('advisor and supervisor', APPLICATION_ACCEPTED_NO_ADVISOR, etc.) while
consistently substituting the literal 'Best regards / Coordination Team'
sign-off with the new <div th:utext="${emailSignature}"></div> placeholder.
- Revert the in-place edits to migrations 14 / 19 / 28: those changesets
have already been applied to deployed databases so editing them would
trip Liquibase's MD5 checksum guard. Migration 40 already covers the
data-side rewrite via REPLACE.
- Add @JsonInclude(NON_EMPTY) to ResearchGroupSettingsEmailDTO to match
the codebase's DTO serialization convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR standardizes email footers by introducing a shared notification-settings footer appended in MailBuilder.send(), and adds a per–research group configurable email signature that templates can render via ${emailSignature}.
Changes:
- Add
email_signaturetoresearch_group_settingsand expose it via DTO/controller + client types. - Update stored/default email templates to use
${emailSignature}and remove per-template notification settings blocks. - Append a centralized notification-settings footer to every rendered email and add e2e assertions for it.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/main/resources/db/changelog/manual/insert_tum_default_email_templates.sql | Replaces hardcoded sign-off with ${emailSignature} in default TUM templates. |
| server/src/main/resources/db/changelog/manual/insert_tum_aet_email_templates.sql | Replaces ${config.signature} and removes per-template notification blocks in AET templates. |
| server/src/main/resources/db/changelog/db.changelog-master.xml | Registers Liquibase migration 40_consistent_email_footer.sql. |
| server/src/main/resources/db/changelog/changes/40_consistent_email_footer.sql | Adds email_signature column and bulk-updates existing email_templates contents. |
| server/src/main/java/de/tum/cit/aet/thesis/utility/MailBuilder.java | Adds default emailSignature, withSignature(...), and appends centralized notification footer after render. |
| server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java | Applies research-group signature to mail builders where a group is available. |
| server/src/main/java/de/tum/cit/aet/thesis/entity/ResearchGroupSettings.java | Adds emailSignature field mapped to email_signature column. |
| server/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.java | Extends email settings DTO with emailSignature. |
| server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java | Persists emailSignature from settings payload (trim + null when blank). |
| client/src/requests/responses/researchGroupSettings.ts | Extends client type for research group email settings with emailSignature. |
| client/e2e/mailpit.ts | Adds assertEmailFooter helper to check presence of centralized footer/link. |
| client/e2e/thesis-workflow.spec.ts | Uses assertEmailFooter in thesis creation workflow. |
| client/e2e/thesis-grading-workflow.spec.ts | Uses assertEmailFooter in grading workflow. |
| client/e2e/proposal-feedback-workflow.spec.ts | Uses assertEmailFooter in proposal workflows. |
| client/e2e/presentation-workflow.spec.ts | Uses assertEmailFooter in presentation workflow. |
| client/e2e/application-workflow.spec.ts | Uses assertEmailFooter in application submission workflow. |
| client/e2e/application-review-workflow.spec.ts | Uses assertEmailFooter in review/accept/reject workflows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche Most of the previous feedback is addressed — 14/19/28 are reverted, DTO has @JsonInclude, signature is read in the controller. But the merge commit re-introduced the same Liquibase checksum issue on a different changeset: insert_tum_default_email_templates.sql is included via sqlFile from changeset ramona:21-seed-default-email-templates, so modifying it will trip the MD5 guard on production startup. Easy fix — revert that file too, migration 40 already covers the data side. See inline.
Five threads from review:
1. CRITICAL — Liquibase checksum (Claudia). The seed file
`insert_tum_default_email_templates.sql` is loaded via `sqlFile` from
the already-applied `ramona:21-seed-default-email-templates`
changeset, with no `runOnChange`. Modifying its content changes the
MD5 Liquibase validates on startup, breaking deployed databases.
Revert the file to develop's content. Migration 40 already rewrites
the literal "Best regards / Coordination Team" sign-off to
`${emailSignature}` via REPLACE for both fresh and existing DBs, so
the end state is identical.
2. Notification-link href in MailBuilder.send() interpolated
`config.getClientHost()` directly into an HTML attribute. HTML-escape
the URL via Spring's HtmlUtils and add `rel="noopener noreferrer
nofollow"` for parity with the other Anchor tags in the codebase.
3. prepareMinimalApplicationMailBuilder did not include the
`${emailSignature}` placeholder, so emails sent through the minimal
path skipped the per-research-group signature. Append the placeholder
so the consistent-footer behaviour holds for all mail paths.
4. (Self) Server-rendered HTML signature is admin-supplied — a known
trust-boundary choice for a per-group signature feature, intentionally
not sanitized so admins can embed images/links.
5. (Doc-only) PR description references migration #36; the migration was
renumbered to #40 during the develop merge — to be updated on the PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
krusche
left a comment
There was a problem hiding this comment.
On the th:utext signature thread (admin-controlled HTML rendering): this is an intentional design choice. The signature is set via the research group settings API which is already restricted to group admins, who likewise control the email templates themselves. HTML embedding (images, links, formatted contact info) is the explicit goal of the feature — sanitizing to a plain-text or strict allowlist would defeat the purpose. Marking the thread as resolved.
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche All previous feedback addressed — already-applied changesets (14/19/28) and the seed SQL referenced by changeset 21 are back to develop's content, the DTO has @JsonInclude(NON_EMPTY), and migration 40's REPLACE handles the data transform for deployed databases. Footer URL is HTML-escaped centrally in MailBuilder.send(). The th:utext signature design choice is reasonable given the API's group-admin restriction. Nice work.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
client/src/requests/responses/researchGroupSettings.ts (1)
8-8: Grading scheme changes appear unrelated to this PR's email footer objectives.This field addition is not mentioned in the PR objectives, which focus exclusively on email footer and signature functionality. Consider moving grading scheme changes to a separate PR for clearer change tracking.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/requests/responses/researchGroupSettings.ts` at line 8, The addition of the gradingSchemeSettings field (gradingSchemeSettings?: IResearchGroupSettingsGradingScheme) in client/src/requests/responses/researchGroupSettings.ts is unrelated to the email footer/signature work; remove this field from the current diff or extract it into a separate branch/PR so the email-footer PR only contains signature/footer changes; specifically revert or delete the gradingSchemeSettings line from the ResearchGroupSettings response interface and, if needed, open a new PR that introduces IResearchGroupSettingsGradingScheme and gradingSchemeSettings with its own description and tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@client/src/requests/responses/researchGroupSettings.ts`:
- Line 8: The addition of the gradingSchemeSettings field
(gradingSchemeSettings?: IResearchGroupSettingsGradingScheme) in
client/src/requests/responses/researchGroupSettings.ts is unrelated to the email
footer/signature work; remove this field from the current diff or extract it
into a separate branch/PR so the email-footer PR only contains signature/footer
changes; specifically revert or delete the gradingSchemeSettings line from the
ResearchGroupSettings response interface and, if needed, open a new PR that
introduces IResearchGroupSettingsGradingScheme and gradingSchemeSettings with
its own description and tests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 09f99808-5c6c-49d1-b6f5-453bf5050ed0
📒 Files selected for processing (8)
client/e2e/application-review-workflow.spec.tsclient/e2e/application-workflow.spec.tsclient/e2e/mailpit.tsclient/e2e/presentation-workflow.spec.tsclient/e2e/proposal-feedback-workflow.spec.tsclient/e2e/thesis-grading-workflow.spec.tsclient/e2e/thesis-workflow.spec.tsclient/src/requests/responses/researchGroupSettings.ts
✅ Files skipped from review due to trivial changes (1)
- client/e2e/thesis-grading-workflow.spec.ts
Summary
MailBuilder.send()emailSignaturefield toResearchGroupSettingsso each research group can customize their sign-off${config.signature}references with the${emailSignature}Thymeleaf variableTest plan
./gradlew test)./execute-e2e-local.sh), including newassertEmailFooterassertions in 6 spec filesemailSignaturefrom research group settings overrides the default🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes