diff --git a/client/e2e/application-review-workflow.spec.ts b/client/e2e/application-review-workflow.spec.ts
index 26d5b2c98..40fa76757 100644
--- a/client/e2e/application-review-workflow.spec.ts
+++ b/client/e2e/application-review-workflow.spec.ts
@@ -6,6 +6,7 @@ import {
getBody,
getToAddresses,
assertSentFromApp,
+ assertEmailFooter,
findBySubject,
} from './mailpit'
@@ -62,6 +63,7 @@ test.describe('Application Review Workflow', () => {
const rejectionEmail = findBySubject(newEmails, 'Thesis Application Rejection')
expect(rejectionEmail, 'Rejection email with correct subject should be sent').toBeDefined()
assertSentFromApp(rejectionEmail!)
+ assertEmailFooter(rejectionEmail!)
expect(getToAddresses(rejectionEmail!)).toContain('student4@test.local')
// Body should greet the student by first name and reference the topic title
@@ -131,6 +133,7 @@ test.describe('Application Review Workflow', () => {
const acceptanceEmail = findBySubject(newEmails, 'Thesis Application Acceptance')
expect(acceptanceEmail, 'Acceptance email should be sent').toBeDefined()
assertSentFromApp(acceptanceEmail!)
+ assertEmailFooter(acceptanceEmail!)
expect(getToAddresses(acceptanceEmail!)).toContain('student5@test.local')
// Body should greet student, mention the supervisor, and include a thesis link
@@ -143,6 +146,7 @@ test.describe('Application Review Workflow', () => {
const thesisEmail = findBySubject(newEmails, 'Thesis Created')
expect(thesisEmail, 'Thesis creation email should be sent').toBeDefined()
assertSentFromApp(thesisEmail!)
+ assertEmailFooter(thesisEmail!)
expect(getToAddresses(thesisEmail!)).toContain('student5@test.local')
// Body should contain the thesis title and a link to the thesis
diff --git a/client/e2e/application-workflow.spec.ts b/client/e2e/application-workflow.spec.ts
index 53f115177..9ee423c6e 100644
--- a/client/e2e/application-workflow.spec.ts
+++ b/client/e2e/application-workflow.spec.ts
@@ -12,6 +12,7 @@ import {
getBody,
getToAddresses,
assertSentFromApp,
+ assertEmailFooter,
findBySubject,
} from './mailpit'
@@ -131,6 +132,7 @@ test.describe('Application Workflow - Student submits application', () => {
const studentEmail = findBySubject(newStudentEmails, 'Thesis Application Confirmation')
expect(studentEmail, 'Student confirmation email should be sent').toBeDefined()
assertSentFromApp(studentEmail!)
+ assertEmailFooter(studentEmail!)
expect(getToAddresses(studentEmail!)).toContain('student@test.local')
const studentBody = getBody(studentEmail!)
@@ -151,6 +153,7 @@ test.describe('Application Workflow - Student submits application', () => {
'Examiner (examiner@test.local) should receive "New Thesis Application" email',
).toBeDefined()
assertSentFromApp(examinerChairEmail!)
+ assertEmailFooter(examinerChairEmail!)
const examinerBody = getBody(examinerChairEmail!)
expect(examinerBody, 'Examiner email should mention the applicant').toContain('Student')
diff --git a/client/e2e/mailpit.ts b/client/e2e/mailpit.ts
index 4a77abd22..050d45369 100644
--- a/client/e2e/mailpit.ts
+++ b/client/e2e/mailpit.ts
@@ -137,6 +137,19 @@ export function assertBodyContains(message: MailpitMessage, expectedText: string
expect(body, `Expected email body to contain "${expectedText}"`).toContain(expectedText)
}
+/**
+ * Assert that the email contains the standard footer with notification settings link.
+ */
+export function assertEmailFooter(message: MailpitMessage): void {
+ const body = getBody(message)
+ expect(body, 'Expected email to contain notification settings link').toContain(
+ 'notification settings in Thesis Management',
+ )
+ expect(body, 'Expected email to contain /settings/notifications link').toContain(
+ '/settings/notifications',
+ )
+}
+
/**
* Assert that the email was sent from the application sender.
* Verifies the From header matches the expected format: "ThesisManagement <...@...>"
diff --git a/client/e2e/presentation-workflow.spec.ts b/client/e2e/presentation-workflow.spec.ts
index 154e731f6..43d48d248 100644
--- a/client/e2e/presentation-workflow.spec.ts
+++ b/client/e2e/presentation-workflow.spec.ts
@@ -6,6 +6,7 @@ import {
getBody,
getToAddresses,
assertSentFromApp,
+ assertEmailFooter,
} from './mailpit'
// Thesis d000-0003 is in ASSESSED state, assigned to student3, has abstract text set
@@ -170,6 +171,7 @@ test.describe.serial('Presentation Workflow', () => {
'New Presentation scheduled',
)
assertSentFromApp(privateEmail)
+ assertEmailFooter(privateEmail)
expect(getToAddresses(privateEmail)).toContain('student3@test.local')
const body = getBody(privateEmail)
diff --git a/client/e2e/proposal-feedback-workflow.spec.ts b/client/e2e/proposal-feedback-workflow.spec.ts
index e8841471c..a4227526b 100644
--- a/client/e2e/proposal-feedback-workflow.spec.ts
+++ b/client/e2e/proposal-feedback-workflow.spec.ts
@@ -7,6 +7,7 @@ import {
getBody,
getToAddresses,
assertSentFromApp,
+ assertEmailFooter,
hasAttachment,
} from './mailpit'
@@ -58,6 +59,7 @@ test.describe('Proposal Upload - Student uploads proposal', () => {
const proposalEmail = newEmails.find((e) => getSubject(e) === 'Thesis Proposal Added')
expect(proposalEmail, 'Proposal upload email with correct subject should be sent').toBeDefined()
assertSentFromApp(proposalEmail!)
+ assertEmailFooter(proposalEmail!)
expect(getToAddresses(proposalEmail!)).toContain('supervisor@test.local')
// Body should reference the thesis title, the uploader name, and include a link
@@ -126,6 +128,7 @@ test.describe('Proposal Feedback - Supervisor requests changes', () => {
'Change request email with correct subject should be sent',
).toBeDefined()
assertSentFromApp(changeRequestEmail!)
+ assertEmailFooter(changeRequestEmail!)
expect(getToAddresses(changeRequestEmail!)).toContain('student2@test.local')
// Body should greet the student, reference the thesis title, mention the reviewer,
diff --git a/client/e2e/thesis-grading-workflow.spec.ts b/client/e2e/thesis-grading-workflow.spec.ts
index 3928d4115..10db435ca 100644
--- a/client/e2e/thesis-grading-workflow.spec.ts
+++ b/client/e2e/thesis-grading-workflow.spec.ts
@@ -7,6 +7,7 @@ import {
getBody,
getToAddresses,
assertSentFromApp,
+ assertEmailFooter,
} from './mailpit'
// Thesis d000-0003: ASSESSED state, student3, supervisor2, examiner2 (DSA group)
@@ -181,6 +182,7 @@ test.describe.serial('Thesis Grading Workflow', () => {
const gradeEmail = newEmails.find((e) => getSubject(e) === 'Final Grade available for Thesis')
expect(gradeEmail, 'Final grade email should be sent').toBeDefined()
assertSentFromApp(gradeEmail!)
+ assertEmailFooter(gradeEmail!)
expect(getToAddresses(gradeEmail!)).toContain('student3@test.local')
// Body should contain: greeting, thesis title, examiner name, final grade,
diff --git a/client/e2e/thesis-workflow.spec.ts b/client/e2e/thesis-workflow.spec.ts
index 2d378db67..6241ff5e8 100644
--- a/client/e2e/thesis-workflow.spec.ts
+++ b/client/e2e/thesis-workflow.spec.ts
@@ -7,6 +7,7 @@ import {
getBody,
getToAddresses,
assertSentFromApp,
+ assertEmailFooter,
} from './mailpit'
test.describe('Thesis Workflow - Examiner creates a thesis', () => {
@@ -66,6 +67,7 @@ test.describe('Thesis Workflow - Examiner creates a thesis', () => {
const creationEmail = newEmails.find((e) => getSubject(e) === 'Thesis Created')
expect(creationEmail, 'Thesis creation email should be sent').toBeDefined()
assertSentFromApp(creationEmail!)
+ assertEmailFooter(creationEmail!)
expect(getToAddresses(creationEmail!)).toContain('student4@test.local')
// Body should contain: greeting, thesis title, examiner/supervisor/student names, and link
diff --git a/client/src/requests/responses/researchGroupSettings.ts b/client/src/requests/responses/researchGroupSettings.ts
index 15333b28b..07d9412f9 100644
--- a/client/src/requests/responses/researchGroupSettings.ts
+++ b/client/src/requests/responses/researchGroupSettings.ts
@@ -23,6 +23,7 @@ export interface IResearchGroupSettingsPhase {
export interface IResearchGroupSettingsEmail {
applicationNotificationEmail?: string | null
+ emailSignature?: string | null
}
export interface IResearchGroupSettingsWritingGuide {
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java b/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java
index e8100bb11..8e678648a 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java
@@ -82,6 +82,8 @@ public ResponseEntity
You can view the full application details here: " + "" - + applicationUrl + "
"; - - return new MailBuilder(config, subject, body); + + applicationUrl + "" + // Match the rest of the templates (which all end with the + // emailSignature placeholder so the per-research-group + // signature gets injected by Thymeleaf). + + ""; + + MailBuilder builder = new MailBuilder(config, subject, body); + applyGroupSignature(builder, application.getResearchGroup()); + return builder; } /** @@ -228,6 +237,7 @@ public void sendApplicationAcceptanceEmail(Application application, Thesis thesi templateCase, "en"); MailBuilder mailBuilder = new MailBuilder(config, emailTemplate.getSubject(), emailTemplate.getBodyHtml()); + applyGroupSignature(mailBuilder, application.getResearchGroup()); mailBuilder .addPrimaryRecipient(application.getUser()) .addSecondaryRecipient(advisor) @@ -250,6 +260,7 @@ public void sendApplicationRejectionEmail(Application application, ApplicationRe reason.getTemplateCase(), "en"); MailBuilder mailBuilder = new MailBuilder(config, emailTemplate.getSubject(), emailTemplate.getBodyHtml()); + applyGroupSignature(mailBuilder, application.getResearchGroup()); mailBuilder .addPrimaryRecipient(application.getUser()) .addDefaultBccRecipients(application.getResearchGroup().getHead().getEmail()) @@ -269,6 +280,7 @@ public void sendApplicationReminderEmail(User user, long unreviewedApplications) "APPLICATION_REMINDER", "en"); MailBuilder mailBuilder = new MailBuilder(config, emailTemplate.getSubject(), emailTemplate.getBodyHtml()); + applyGroupSignature(mailBuilder, user.getResearchGroup()); mailBuilder .addPrimaryRecipient(user) .addNotificationName("unreviewed-application-reminder") @@ -294,6 +306,7 @@ public void sendApplicationAutomaticRejectReminderEmail(User user, ListRegards,
The Thesis Management Team.
Best regards,
' || chr(10) || 'The Thesis Coordination Team
Dear [[${recipient.firstName}]],
@@ -93,7 +93,7 @@ project. You can view your thesis details and tasks on: [[${thesisUrl}]] -', 'Application was accepted with same supervisor and examiner'), +', 'Application was accepted with same supervisor and examiner'), ('APPLICATION_CREATED_CHAIR', 'New Thesis Application', 'Dear [[${recipient.firstName}]],
there is a new thesis application by [[${application.applicantFirstName}]] [[${application.applicantLastName}]].
@@ -158,11 +158,7 @@ Full Details: You can find the submitted files in the attachment part of this email. -Dear [[${recipient.firstName}]],
@@ -229,7 +225,9 @@ and research commitments of our group may result in a response time of up to fou We appreciate your patience and understanding during this period. -You can find the submitted files in the attachment part of this email.
', +You can find the submitted files in the attachment part of this email.
+ +', 'Confirmation email to the applying student when application was submitted'), ('APPLICATION_REJECTED', 'Thesis Application Rejection', 'Dear [[${recipient.firstName}]],
@@ -244,7 +242,7 @@ The volume of applications received this year was exceptionally high, and I have I recommend reaching out to other faculty members who may align more closely with your qualifications and area of interest. -', 'Application was rejected'), +', 'Application was rejected'), ('APPLICATION_REJECTED_TOPIC_REQUIREMENTS', 'Thesis Application Rejection', 'Dear [[${recipient.firstName}]],
@@ -256,7 +254,7 @@ After reviewing your application and supporting documents, I regret to inform yo I recommend considering other thesis topics or reaching out to faculty members whose research focus may better align with your qualifications.
-', 'Application was rejected because topic requirements were not met'), +', 'Application was rejected because topic requirements were not met'), ('APPLICATION_REJECTED_STUDENT_REQUIREMENTS', 'Thesis Application Rejection', 'Dear [[${recipient.firstName}]],
@@ -269,7 +267,7 @@ Unfortunately, I must inform you that you do not currently meet the necessary re I recommend reaching out to other faculty members who may align more closely with your qualifications and area of interest.
-', +', 'Application was rejected because student does not fulfil chair''s requirements'), ('APPLICATION_REJECTED_TITLE_NOT_INTERESTING', 'Thesis Application Rejection', 'Dear [[${recipient.firstName}]],
@@ -283,7 +281,7 @@ However, the suggested topic does not align with the current research interests I encourage you to explore other faculty members whose research is more closely related to your proposed area of study. -', +', 'Application was rejected because the suggested thesis title is not interesting'), ('APPLICATION_REJECTED_TOPIC_FILLED', 'Thesis Application Rejection', 'Dear [[${recipient.firstName}]],
@@ -300,7 +298,7 @@ We found a student for the specific topic you applied for. You can explore other topics or suggest a topic yourself in your area of interest. -', 'Application was rejected because topic was closed'), +', 'Application was rejected because topic was closed'), ('APPLICATION_REJECTED_TOPIC_OUTDATED', 'Thesis Application Rejection', 'Dear [[${recipient.firstName}]],
@@ -316,7 +314,7 @@ I wanted to inform you that the topic you applied for is no longer available. You can explore other topics or suggest a topic yourself in your area of interest.
-', 'Application was rejected because topic is outdated'), +', 'Application was rejected because topic is outdated'), ('APPLICATION_REMINDER', 'Unreviewed Thesis Applications', 'Dear [[${recipient.firstName}]],
@@ -327,11 +325,7 @@ There are currently [[${unreviewedApplications}]] unreviewed th Review Applications: [[${reviewApplicationsLink}]]
-Dear [[${recipient.firstName}]],
@@ -362,11 +356,7 @@ Review Applications: -Dear [[${recipient.firstName}]],
@@ -378,11 +368,7 @@ Please contact your supervisor or examiner if you think that this was a mistake. Full Details:
-Dear [[${recipient.firstName}]],
@@ -398,11 +384,7 @@ Please contact your supervisor or examiner if you think that this was a mistake. Full Details:
-Dear [[${recipient.firstName}]],
@@ -419,7 +401,9 @@ Please contact your supervisor or examiner if you think that this was a mistake.The next step is that you write a proposal and submit it on [[${thesisUrl}]] -
', 'New thesis was created and assigned to a student'), + + +', 'New thesis was created and assigned to a student'), ('THESIS_FINAL_GRADE', 'Final Grade available for Thesis', 'Dear [[${recipient.firstName}]],
@@ -439,11 +423,7 @@ The next step is that you write a proposal and submit it on
-Dear [[${recipient.firstName}]],
@@ -458,11 +438,7 @@ The next step is to write an assessment about the thesis. Full Details:
-Dear [[${recipient.firstName}]],
@@ -473,11 +449,7 @@ The next step is to write an assessment about the thesis. Full Details:
-Dear [[${recipient.firstName}]],
@@ -513,11 +485,7 @@ The next step is to write an assessment about the thesis. Full Details:
-Dear [[${recipient.firstName}]],
@@ -553,11 +521,7 @@ The next step is to write an assessment about the thesis. Full Details:
-Dear [[${recipient.firstName}]],
@@ -604,11 +564,7 @@ The [[${thesis.type}]] thesis presentation of [[${presentation.scheduledAt}]] was cancelled.
-Dear [[${recipient.firstName}]],
@@ -655,11 +607,7 @@ The next step is to start with the project work and with writing the thesis. You can see your submission deadline on [[${thesisUrl}]].
-Dear [[${recipient.firstName}]],
@@ -677,11 +625,7 @@ The following changes were requested:
Full Details:
Dear [[${recipient.firstName}]],
@@ -693,11 +637,7 @@ You can find the submitted file in the attachment part of this email. Full Details:
-