From 988028688d924771e7a3c97ec046851f9f95cdc9 Mon Sep 17 00:00:00 2001
From: Manoj Garai
Date: Fri, 24 Apr 2026 15:55:59 +0100
Subject: [PATCH 01/10] Email notification for cert expiry
---
src/main/java/uk/ac/ngs/TaskConfig.java | 75 +++++++++++++
src/main/java/uk/ac/ngs/WebConfig.java | 1 +
.../uk/ac/ngs/dao/JdbcCertificateDao.java | 47 ++++++++
.../uk/ac/ngs/service/CertificateService.java | 42 +++++++
.../uk/ac/ngs/service/email/EmailService.java | 37 +++++++
.../emailUserCertExpiryReminderTemplate.html | 42 +++++++
.../uk/ac/ngs/dao/JdbcCertificateDaoTest.java | 104 ++++++++++++++++++
7 files changed, 348 insertions(+)
create mode 100644 src/main/java/uk/ac/ngs/TaskConfig.java
create mode 100644 src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
diff --git a/src/main/java/uk/ac/ngs/TaskConfig.java b/src/main/java/uk/ac/ngs/TaskConfig.java
new file mode 100644
index 0000000..96240f3
--- /dev/null
+++ b/src/main/java/uk/ac/ngs/TaskConfig.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2015 STFC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.ac.ngs;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.annotation.SchedulingConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+
+import uk.ac.ngs.service.CertificateService;
+
+@Configuration
+@EnableScheduling
+public class TaskConfig implements SchedulingConfigurer {
+ public static final Logger log = LoggerFactory.getLogger(TaskConfig.class);
+
+ private CertificateService certificateService;
+
+ public TaskConfig(CertificateService certificateService) {
+ this.certificateService = certificateService;
+ }
+
+ @Scheduled(cron = "0 44 15 * * ?")
+ public void runDailyCertificateExpiryReminderJob() {
+
+ log.info("Starting daily certificate expiry reminder job");
+
+ long startTime = System.currentTimeMillis();
+
+ try {
+ certificateService.sendCertificateExpiryReminders();
+
+ long durationMs = System.currentTimeMillis() - startTime;
+ log.info("Completed daily certificate expiry reminder job successfully in {} ms",
+ durationMs);
+
+ } catch (Exception ex) {
+ log.error("Daily certificate expiry reminder job failed", ex);
+ throw ex;
+ }
+
+ }
+
+ @Override
+ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
+ taskRegistrar.setScheduler(taskScheduler());
+ }
+
+
+ @Bean
+ public ThreadPoolTaskScheduler taskScheduler() {
+ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+ scheduler.setPoolSize(5);
+ scheduler.setThreadNamePrefix("cert-expiry-scheduler-");
+ scheduler.setWaitForTasksToCompleteOnShutdown(true);
+ scheduler.setAwaitTerminationSeconds(30);
+ return scheduler;
+ }
+}
diff --git a/src/main/java/uk/ac/ngs/WebConfig.java b/src/main/java/uk/ac/ngs/WebConfig.java
index d9df489..ec142d5 100644
--- a/src/main/java/uk/ac/ngs/WebConfig.java
+++ b/src/main/java/uk/ac/ngs/WebConfig.java
@@ -120,6 +120,7 @@ public EmailService emailService() {
emailService.setEmailOnRaopRoleRequestApprovalTemplate("emailOnRaopRoleRequestApprovalTemplate.html");
emailService.setEmailOnRaopRoleRequestRejectionTemplate("emailOnRaopRoleRequestRejectionTemplate.html");
emailService.setEmailOnRoleChangeToUserTemplate("emailOnRoleChangeToUserTemplate.html");
+ emailService.setEmailUserCertExpiryReminderTemplate("emailUserCertExpiryReminderTemplate.html");
emailService.setBasePortalUrl(basePortalUrl);
return emailService;
}
diff --git a/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java b/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java
index 90f14c5..9c7b1eb 100644
--- a/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java
+++ b/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java
@@ -34,6 +34,9 @@
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -458,6 +461,50 @@ public String updateDataCol_LastActionDateRaop(String data, long raopId) {
}
+ /**
+ * Retrieves all valid certificates that are expiring exactly in the given
+ * number of days.
+ *
+ *
+ * This method is intended for reminder notifications (e.g. 7-day expiry
+ * reminder).
+ * A certificate is considered matching if its {@code notafter} timestamp falls
+ * within the full UTC day that is {@code daysToExpire} days from now.
+ *
+ *
+ * @param daysToExpire number of days from today (e.g. 7 for a 7-day reminder)
+ * @return list of certificates expiring exactly in the specified number of days
+ */
+ public List getValidCertificatesExpiringInDays(int daysToExpire) {
+
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC);
+
+ LocalDate targetDate = LocalDate.now(ZoneOffset.UTC).plusDays(daysToExpire);
+
+ long startOfDay = Long.parseLong(
+ formatter.format(targetDate.atStartOfDay(ZoneOffset.UTC)));
+
+ long endOfDay = Long.parseLong(
+ formatter.format(targetDate.plusDays(1)
+ .atStartOfDay(ZoneOffset.UTC)));
+
+ Map params = Map.of(
+ "startOfDay", startOfDay,
+ "endOfDay", endOfDay);
+
+ String sql = """
+ SELECT cert_key, 'data' as data, dn, cn, email, status, role, notafter
+ FROM certificate
+ WHERE status = 'VALID'
+ AND notafter >= :startOfDay
+ AND notafter < :endOfDay
+ ORDER BY notafter ASC
+ """;
+
+ return jdbcTemplate.query(sql, params, new CertificateRowMapper());
+ }
+
+
/**
* Build up the query using the given where by parameters in the map
* and return the query and the named parameter map for subsequent parameter-binding/execution.
diff --git a/src/main/java/uk/ac/ngs/service/CertificateService.java b/src/main/java/uk/ac/ngs/service/CertificateService.java
index 8dc7f81..9fc8222 100644
--- a/src/main/java/uk/ac/ngs/service/CertificateService.java
+++ b/src/main/java/uk/ac/ngs/service/CertificateService.java
@@ -245,6 +245,48 @@ private void sendEmailNotificationOnRoleChangeRequest(CertificateRow targetCert,
this.emailService.sendUserOnRaopRoleRequest(requesterCN, targetCN, targetEmail);
}
+
+ /**
+ * Sends certificate expiry reminder emails for certificates expiring
+ * in 7 days and 30 days.
+ *
+ *
+ * This method is intended to be executed by a scheduled job
+ * once per day.
+ *
+ */
+ public void sendCertificateExpiryReminders() {
+
+ sendRemindersForDaysToExpire(7);
+ sendRemindersForDaysToExpire(30);
+ }
+
+ private void sendRemindersForDaysToExpire(int daysToExpire) {
+
+ List certificates = jdbcCertDao.getValidCertificatesExpiringInDays(daysToExpire);
+
+ int count = certificates.size();
+
+ if (count > 0) {
+ log.info(count + " certificate(s) are going to expire after " + daysToExpire + " days.");
+ } else {
+ log.info("No certificates are going to expire after " + daysToExpire + " days.");
+ }
+
+ for (CertificateRow cert : certificates) {
+ log.info("Sending " + daysToExpire + "-days expiry reminder for cert [" + cert.getCert_key() + "] to "
+ + cert.getEmail());
+
+ this.emailService.sendEmailReminderToUserOnCertExpiry(
+ cert.getCert_key(),
+ daysToExpire,
+ cert.getCn(),
+ cert.getDn(),
+ cert.getEmail());
+ }
+ }
+
+
@Inject
public void setJdbcCertificateDao(JdbcCertificateDao jdbcCertDao) {
this.jdbcCertDao = jdbcCertDao;
diff --git a/src/main/java/uk/ac/ngs/service/email/EmailService.java b/src/main/java/uk/ac/ngs/service/email/EmailService.java
index 023d1c9..80165e5 100644
--- a/src/main/java/uk/ac/ngs/service/email/EmailService.java
+++ b/src/main/java/uk/ac/ngs/service/email/EmailService.java
@@ -55,6 +55,7 @@ public class EmailService {
private String emailOnRaopRoleRequestApprovalTemplate;
private String emailOnRaopRoleRequestRejectionTemplate;
private String emailOnRoleChangeToUserTemplate;
+ private String emailUserCertExpiryReminderTemplate;
private String basePortalUrl;
@@ -490,6 +491,35 @@ public void sendEmailOnRaopRoleRequestRejection(String recipient, String actorCN
}
}
+
+/**
+ * Sends an email reminder to the user informing them of an upcoming
+ * certificate expiry.
+ *
+ * @param certKey certificate identifier
+ * @param daysToExpire number of days remaining before expiry
+ * @param dn certificate distinguished name
+ * @param recipientEmail recipient's email address
+ */
+
+ public void sendEmailReminderToUserOnCertExpiry(long certKey, int daysToExpire, String cn, String dn, String recipientEmail) {
+ SimpleMailMessage msg = new SimpleMailMessage(this.emailTemplate);
+ msg.setTo(recipientEmail);
+ Map vars = new HashMap<>();
+
+ vars.put("certKey", certKey);
+ vars.put("daysToExpire", daysToExpire);
+ vars.put("cn", cn);
+ vars.put("dn", dn);
+ vars.put("basePortalUrl", basePortalUrl);
+
+ try {
+ this.mailSender.send(msg, vars, this.emailUserCertExpiryReminderTemplate);
+ } catch (MailException ex) {
+ log.error("MailSender " + ex.getMessage());
+ }
+ }
+
/**
* Email on RAOP to user role change.
*
@@ -649,4 +679,11 @@ public void setEmailOnRoleChangeToUserTemplate(String emailOnRoleChangeToUserTem
this.emailOnRoleChangeToUserTemplate = emailOnRoleChangeToUserTemplate;
}
+ /**
+ * @param emailUserCertExpiryReminderTemplate the emailUserCertExpiryReminderTemplate to set
+ */
+ public void setEmailUserCertExpiryReminderTemplate(String emailUserCertExpiryReminderTemplate) {
+ this.emailUserCertExpiryReminderTemplate = emailUserCertExpiryReminderTemplate;
+ }
+
}
diff --git a/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html b/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
new file mode 100644
index 0000000..f0d5133
--- /dev/null
+++ b/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Dear Owner of UK e-Science Certificate number: ${certKey},
+
+
+ You have a certificate which will expire in ${daysToExpire} days. If you do not wish to renew this
+ certificates or if you have
+ done so already then please ignore this message.
+
+
+ Your certificate was issued for "${cn}" and has the subject:
+
+
+ [${dn}]
+
+
+ If you prefer a browser interface for your certificate management and you have your current certificate in your
+ browser then please go to the CA Portal ${basePortalUrl} and follow the instructions
+ for renewal.
+
+
+ Other tools are available at https://ca.grid-support.ac.uk
+
+
+ The server will then queue your renewal request for your RA to approve.
+ There is NO need to visit your RA. You will receive an e-mail when the certificate is available.
+
+
+ If you have any questions, concerns or comments, then we shall be pleased to hear them. We are always aiming to
+ improve our service, and so your feedback will be appreciated.
+
+
+ Regards,
+
+
+ The UK eScience CA Support Centre
+
+
+
\ No newline at end of file
diff --git a/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java b/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java
index 22c3531..1fcbd7f 100644
--- a/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java
+++ b/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java
@@ -12,6 +12,9 @@
*/
package uk.ac.ngs.dao;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -28,6 +31,7 @@
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -157,4 +161,104 @@ public void testFindActiveCAs_withValidInputs_returnsResults() {
assertTrue(capturedQuery.contains("notafter > :current_time"));
}
+
+ @Test
+ public void shouldReturnCertificatesExpiringInSevenDays() {
+ // given
+ int daysToExpire = 7;
+
+ CertificateRow cert1 = new CertificateRow();
+ cert1.setCert_key(201L);
+ cert1.setDn("dn1");
+ cert1.setCn("cn1");
+ cert1.setEmail("a@test.com");
+ CertificateRow cert2 = new CertificateRow();
+ cert1.setCert_key(202L);
+ cert1.setDn("dn2");
+ cert1.setCn("cn2");
+ cert1.setEmail("b@test.com");
+
+ List expectedRows = List.of(cert1, cert2);
+
+ when(jdbcTemplate.query(anyString(), anyMap(), ArgumentMatchers.>any()))
+ .thenReturn(expectedRows);
+
+ // when
+ List result = jdbcCertificateDao.getValidCertificatesExpiringInDays(daysToExpire);
+
+ // then
+ assertNotNull(result);
+ assertEquals(2, result.size());
+ assertEquals(expectedRows, result);
+ }
+
+ @Test
+ public void shouldReturnEmptyListWhenNoCertificatesExpiringInGivenDays() {
+
+ // given
+ when(jdbcTemplate.query(anyString(), anyMap(), ArgumentMatchers.>any()))
+ .thenReturn(List.of());
+
+ // when
+ List result = jdbcCertificateDao.getValidCertificatesExpiringInDays(7);
+
+ // then
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void shouldUseCorrectStartAndEndOfDayForExpiryCheck() {
+
+ String EXPECTED_SQL = """
+ SELECT cert_key, 'data' as data, dn, cn, email, status, role, notafter
+ FROM certificate
+ WHERE status = 'VALID'
+ AND notafter >= :startOfDay
+ AND notafter < :endOfDay
+ ORDER BY notafter ASC
+ """;
+
+ // given
+ ArgumentCaptor
*/
public void sendCertificateExpiryReminders() {
-
sendRemindersForDaysToExpire(7);
sendRemindersForDaysToExpire(30);
}
@@ -277,12 +276,7 @@ private void sendRemindersForDaysToExpire(int daysToExpire) {
log.info("Sending " + daysToExpire + "-days expiry reminder for cert [" + cert.getCert_key() + "] to "
+ cert.getEmail());
- this.emailService.sendEmailReminderToUserOnCertExpiry(
- cert.getCert_key(),
- daysToExpire,
- cert.getCn(),
- cert.getDn(),
- cert.getEmail());
+ this.emailService.sendEmailReminderToUserOnCertExpiry(cert, daysToExpire);
}
}
diff --git a/src/main/java/uk/ac/ngs/service/email/EmailService.java b/src/main/java/uk/ac/ngs/service/email/EmailService.java
index fca89d6..bb69c01 100644
--- a/src/main/java/uk/ac/ngs/service/email/EmailService.java
+++ b/src/main/java/uk/ac/ngs/service/email/EmailService.java
@@ -18,6 +18,7 @@
import org.springframework.mail.SimpleMailMessage;
import uk.ac.ngs.common.CertUtil;
import uk.ac.ngs.domain.CSR_Flags;
+import uk.ac.ngs.domain.CertificateRow;
import javax.inject.Inject;
import java.io.PrintWriter;
@@ -498,20 +499,21 @@ public void sendEmailOnRaopRoleRequestRejection(String recipient, String actorCN
*
* @param certKey certificate identifier
* @param daysToExpire number of days remaining before expiry
+ * @param cn certificate common name
* @param dn certificate distinguished name
* @param recipientEmail recipient's email address
*/
- public void sendEmailReminderToUserOnCertExpiry(long certKey, int daysToExpire, String cn, String dn, String recipientEmail) {
+ public void sendEmailReminderToUserOnCertExpiry(CertificateRow cert, int daysToExpire) {
SimpleMailMessage msg = new SimpleMailMessage(this.emailTemplate);
- msg.setTo(recipientEmail);
+ msg.setTo(cert.getEmail());
msg.setSubject("Your e-Science User Certificate will expire in " + daysToExpire + " days!");
Map vars = new HashMap<>();
- vars.put("certKey", certKey);
+ vars.put("certKey", cert.getCert_key());
vars.put("daysToExpire", daysToExpire);
- vars.put("cn", cn);
- vars.put("dn", dn);
+ vars.put("cn", cert.getCn());
+ vars.put("dn", cert.getDn());
vars.put("basePortalUrl", basePortalUrl);
try {
diff --git a/src/main/resources/application.propertiesTEMPLATE b/src/main/resources/application.propertiesTEMPLATE
index bff4f4f..928c815 100644
--- a/src/main/resources/application.propertiesTEMPLATE
+++ b/src/main/resources/application.propertiesTEMPLATE
@@ -46,6 +46,9 @@ email.password=password
# The portal can (optionally) send emails, this is used for the from address:
email.from=support@grid-support.ac.uk
+#Schedule to run certificate expiry reminder email
+cert.expiry.reminder.cron=0 0 7 * * ?
+
# From https://blog.swdev.ed.ac.uk/2015/06/24/adding-embedded-tomcat-ajp-support-to-a-spring-boot-application/
tomcat.ajp.port=9090
diff --git a/src/test/java/uk/ac/ngs/service/email/EmailServiceTests.java b/src/test/java/uk/ac/ngs/service/email/EmailServiceTests.java
new file mode 100644
index 0000000..fbb6edf
--- /dev/null
+++ b/src/test/java/uk/ac/ngs/service/email/EmailServiceTests.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015 STFC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.ac.ngs.service.email;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.mail.MailSendException;
+import org.springframework.mail.SimpleMailMessage;
+
+import uk.ac.ngs.domain.CertificateRow;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EmailServiceTests {
+
+ @Mock
+ private Sender mailSender;
+
+ @Mock
+ private SimpleMailMessage emailTemplate;
+
+ @InjectMocks
+ private EmailService emailService;
+
+ private CertificateRow cert;
+
+ @Before
+ public void setUp() {
+ cert = new CertificateRow();
+ cert.setCert_key(123);
+ cert.setEmail("user@test.com");
+ cert.setCn("Test CN");
+ cert.setDn("Test DN");
+
+ emailService.setBasePortalUrl("https://portal.test");
+ emailService.setEmailUserCertExpiryReminderTemplate("emailUserCertExpiryReminderTemplate.html");
+ }
+
+ @Test
+ public void shouldSendEmailReminderSuccessfully() {
+
+ int daysToExpire = 7;
+
+ // when
+ emailService.sendEmailReminderToUserOnCertExpiry(cert, daysToExpire);
+
+ // then
+ verify(mailSender).send(
+ argThat(msg -> "user@test.com".equals(msg.getTo()[0]) &&
+ msg.getSubject().contains("7 days")),
+ argThat(vars -> vars.get("certKey").equals(123L) &&
+ vars.get("daysToExpire").equals(7) &&
+ vars.get("cn").equals("Test CN") &&
+ vars.get("dn").equals("Test DN") &&
+ vars.get("basePortalUrl").equals("https://portal.test")),
+ eq("emailUserCertExpiryReminderTemplate.html"));
+ }
+
+ @Test
+ public void shouldHandleMailExceptionGracefully() {
+
+ doThrow(new MailSendException("SMTP error"))
+ .when(mailSender)
+ .send(any(), any(), any());
+
+ // no exception should be thrown
+ assertDoesNotThrow(() -> emailService.sendEmailReminderToUserOnCertExpiry(cert, 7));
+
+ verify(mailSender).send(any(), any(), any());
+ }
+
+}
From 1158d37fbfce50535f1e7cf7d47988955ebd8712 Mon Sep 17 00:00:00 2001
From: Manoj Garai
Date: Tue, 12 May 2026 16:30:03 +0100
Subject: [PATCH 04/10] Catch up with failed cert reminder
---
.../uk/ac/ngs/dao/JdbcCertificateDao.java | 4 +-
.../java/uk/ac/ngs/dao/JobExecutionDao.java | 76 ++++++++++
.../uk/ac/ngs/service/CertificateService.java | 45 +++++-
.../uk/ac/ngs/dao/JdbcCertificateDaoTest.java | 13 +-
.../uk/ac/ngs/dao/JobExecutionDaoTests.java | 142 ++++++++++++++++++
usefulStuff/sql/create_openca_ddl.sql | 10 ++
6 files changed, 279 insertions(+), 11 deletions(-)
create mode 100644 src/main/java/uk/ac/ngs/dao/JobExecutionDao.java
create mode 100644 src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java
diff --git a/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java b/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java
index 50682e6..af79c5c 100644
--- a/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java
+++ b/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java
@@ -475,11 +475,11 @@ public String updateDataCol_LastActionDateRaop(String data, long raopId) {
* @param daysToExpire number of days from today (e.g. 7 for a 7-day reminder)
* @return list of certificates expiring exactly in the specified number of days
*/
- public List getValidCertificatesExpiringInDays(int daysToExpire) {
+ public List getValidCertificatesExpiringInDays(LocalDate processingDate, int daysToExpire) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC);
- LocalDate targetDate = LocalDate.now(ZoneOffset.UTC).plusDays(daysToExpire);
+ LocalDate targetDate = processingDate.plusDays(daysToExpire);
// yyyyMMddHHmmss produces only digits → safe to parse as long
long startOfDay = Long.parseLong(
diff --git a/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java b/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java
new file mode 100644
index 0000000..c31c514
--- /dev/null
+++ b/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015 STFC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package uk.ac.ngs.dao;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+@Repository
+public class JobExecutionDao {
+ private NamedParameterJdbcTemplate jdbcTemplate;
+ private static final Log log = LogFactory.getLog(JobExecutionDao.class);
+
+ public JobExecutionDao(NamedParameterJdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public LocalDate getLastRunDate(String jobName) {
+
+ String sql = """
+ SELECT last_run_date
+ FROM job_execution_tracker
+ WHERE job_name = :jobName
+ """;
+
+ Map params = Map.of("jobName", jobName);
+
+ try {
+ LocalDate result = jdbcTemplate.queryForObject(sql, params,
+ (rs, rowNum) -> rs.getDate("last_run_date").toLocalDate());
+
+ log.info("Last run date for job '" + jobName + "' is " + result);
+ return result;
+
+ } catch (EmptyResultDataAccessException ex) {
+ log.warn("No last run date found for job '" + jobName + "'. Assuming first run.");
+ return null; // first run case
+ }
+ }
+
+ public void updateLastRunDate(String jobName, LocalDate lastRunDate) {
+
+ String sql = """
+ INSERT INTO job_execution_tracker (job_name, last_run_date)
+ VALUES (:jobName, :lastRunDate)
+ ON CONFLICT (job_name)
+ DO UPDATE SET last_run_date = EXCLUDED.last_run_date
+ """;
+
+ Map params = Map.of(
+ "jobName", jobName,
+ "lastRunDate", java.sql.Date.valueOf(lastRunDate));
+
+ int rows = jdbcTemplate.update(sql, params);
+
+ log.info("Updated last run date for job '" + jobName + "' to " + lastRunDate + " (rows affected: " + rows + ")");
+ }
+
+}
diff --git a/src/main/java/uk/ac/ngs/service/CertificateService.java b/src/main/java/uk/ac/ngs/service/CertificateService.java
index c44130c..c60c7d3 100644
--- a/src/main/java/uk/ac/ngs/service/CertificateService.java
+++ b/src/main/java/uk/ac/ngs/service/CertificateService.java
@@ -19,6 +19,7 @@
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
import uk.ac.ngs.common.MutableConfigParams;
+import uk.ac.ngs.dao.JobExecutionDao;
import uk.ac.ngs.dao.JdbcCertificateDao;
import uk.ac.ngs.dao.RoleChangeRequestRepository;
import uk.ac.ngs.domain.CertificateRow;
@@ -29,6 +30,7 @@
import javax.inject.Inject;
import java.io.IOException;
import java.time.LocalDate;
+import java.time.ZoneOffset;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
@@ -48,6 +50,7 @@
public class CertificateService {
private static final Log log = LogFactory.getLog(CertificateService.class);
private JdbcCertificateDao jdbcCertDao;
+ private JobExecutionDao jobExecutionDao;
private EmailService emailService;
private MutableConfigParams mutableConfigParams;
private RoleChangeRequestRepository roleChangeRequestRepository;
@@ -247,22 +250,47 @@ private void sendEmailNotificationOnRoleChangeRequest(CertificateRow targetCert,
/**
- * Sends certificate expiry reminder emails for certificates expiring
- * in 7 days and 30 days.
+ *
+ * Sends certificate expiry reminder emails for 7-day and 30-day intervals,
+ * including catch-up processing for any missed executions.
*
*
* This method is intended to be executed by a scheduled job
* once per day.
*
+ *
+ *
+ * If the scheduler did not run on previous days (e.g. due to downtime),
+ * this method processes all missed dates from the last successful run up to
+ * today, ensuring no expiry reminders are skipped.
+ *
+ *
*/
public void sendCertificateExpiryReminders() {
- sendRemindersForDaysToExpire(7);
- sendRemindersForDaysToExpire(30);
+ LocalDate today = LocalDate.now(ZoneOffset.UTC);
+
+ LocalDate lastRun = jobExecutionDao.getLastRunDate("CERT_EXPIRY_REMINDER");
+
+ // First time case
+ if (lastRun == null) {
+ lastRun = today.minusDays(1);
+ }
+
+ LocalDate processingDate = lastRun.plusDays(1);
+
+ while (!processingDate.isAfter(today)) {
+ sendRemindersForDaysToExpire(processingDate, 7);
+ sendRemindersForDaysToExpire(processingDate, 30);
+ processingDate = processingDate.plusDays(1);
+ }
+
+ jobExecutionDao.updateLastRunDate(
+ "CERT_EXPIRY_REMINDER", today);
}
- private void sendRemindersForDaysToExpire(int daysToExpire) {
+ private void sendRemindersForDaysToExpire(LocalDate processingDate, int daysToExpire) {
- List certificates = jdbcCertDao.getValidCertificatesExpiringInDays(daysToExpire);
+ List certificates = jdbcCertDao.getValidCertificatesExpiringInDays(processingDate, daysToExpire);
int count = certificates.size();
@@ -286,6 +314,11 @@ public void setJdbcCertificateDao(JdbcCertificateDao jdbcCertDao) {
this.jdbcCertDao = jdbcCertDao;
}
+ @Inject
+ public void setJobExecutionDao(JobExecutionDao certExpiryReminderJobExecutionDao) {
+ this.jobExecutionDao = certExpiryReminderJobExecutionDao;
+ }
+
@Inject
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
diff --git a/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java b/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java
index 1fcbd7f..2a6bb79 100644
--- a/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java
+++ b/src/test/java/uk/ac/ngs/dao/JdbcCertificateDaoTest.java
@@ -13,7 +13,9 @@
package uk.ac.ngs.dao;
import java.time.Duration;
+import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
@@ -166,6 +168,7 @@ public void testFindActiveCAs_withValidInputs_returnsResults() {
public void shouldReturnCertificatesExpiringInSevenDays() {
// given
int daysToExpire = 7;
+ LocalDate processingDate = LocalDate.now(ZoneOffset.UTC);
CertificateRow cert1 = new CertificateRow();
cert1.setCert_key(201L);
@@ -184,7 +187,7 @@ public void shouldReturnCertificatesExpiringInSevenDays() {
.thenReturn(expectedRows);
// when
- List result = jdbcCertificateDao.getValidCertificatesExpiringInDays(daysToExpire);
+ List result = jdbcCertificateDao.getValidCertificatesExpiringInDays(processingDate, daysToExpire);
// then
assertNotNull(result);
@@ -194,13 +197,15 @@ public void shouldReturnCertificatesExpiringInSevenDays() {
@Test
public void shouldReturnEmptyListWhenNoCertificatesExpiringInGivenDays() {
+ int daysToExpire = 7;
+ LocalDate processingDate = LocalDate.now(ZoneOffset.UTC);
// given
when(jdbcTemplate.query(anyString(), anyMap(), ArgumentMatchers.>any()))
.thenReturn(List.of());
// when
- List result = jdbcCertificateDao.getValidCertificatesExpiringInDays(7);
+ List result = jdbcCertificateDao.getValidCertificatesExpiringInDays(processingDate, daysToExpire);
// then
assertNotNull(result);
@@ -209,6 +214,8 @@ public void shouldReturnEmptyListWhenNoCertificatesExpiringInGivenDays() {
@Test
public void shouldUseCorrectStartAndEndOfDayForExpiryCheck() {
+ int daysToExpire = 7;
+ LocalDate processingDate = LocalDate.now(ZoneOffset.UTC);
String EXPECTED_SQL = """
SELECT cert_key, 'data' as data, dn, cn, email, status, role, notafter
@@ -229,7 +236,7 @@ public void shouldUseCorrectStartAndEndOfDayForExpiryCheck() {
.thenReturn(List.of());
// when
- jdbcCertificateDao.getValidCertificatesExpiringInDays(7);
+ jdbcCertificateDao.getValidCertificatesExpiringInDays(processingDate, daysToExpire);
// then
Map params = paramsCaptor.getValue();
diff --git a/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java b/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java
new file mode 100644
index 0000000..3e62186
--- /dev/null
+++ b/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java
@@ -0,0 +1,142 @@
+package uk.ac.ngs.dao;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.sql.ResultSet;
+import java.time.LocalDate;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+
+@ExtendWith(MockitoExtension.class)
+public class JobExecutionDaoTests {
+
+ private NamedParameterJdbcTemplate jdbcTemplate;
+ private JobExecutionDao jobExecutionDao;
+
+ @Before
+ public void setUp() {
+ jdbcTemplate = mock(NamedParameterJdbcTemplate.class);
+ jobExecutionDao = new JobExecutionDao(jdbcTemplate);
+ }
+
+ @Test
+ public void shouldReturnLastRunDate() {
+
+ String jobName = "CERT_EXPIRY_REMINDER";
+ LocalDate expectedDate = LocalDate.of(2026, 5, 10);
+
+ when(jdbcTemplate.queryForObject(
+ anyString(),
+ any(Map.class),
+ any(RowMapper.class)))
+ .thenReturn(expectedDate);
+
+ LocalDate result = jobExecutionDao.getLastRunDate(jobName);
+
+ assertNotNull(result);
+ assertEquals(expectedDate, result);
+
+ verify(jdbcTemplate).queryForObject(
+ anyString(),
+ argThat((Map params) -> jobName.equals(params.get("jobName"))),
+ ArgumentMatchers.>any());
+
+ }
+
+ @Test
+ public void shouldReturnNullWhenNoRecordExists() {
+
+ when(jdbcTemplate.queryForObject(
+ anyString(),
+ any(Map.class),
+ any(RowMapper.class)))
+ .thenThrow(new EmptyResultDataAccessException(1));
+
+ LocalDate result = jobExecutionDao.getLastRunDate("CERT_EXPIRY_REMINDER");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void shouldMapSqlDateToLocalDate() throws Exception {
+
+ ResultSet rs = mock(ResultSet.class);
+
+ java.sql.Date sqlDate = java.sql.Date.valueOf("2026-05-10");
+
+ when(rs.getDate("last_run_date")).thenReturn(sqlDate);
+
+ RowMapper mapper = (r, rowNum) -> r.getDate("last_run_date").toLocalDate();
+
+ LocalDate result = mapper.mapRow(rs, 1);
+
+ assertEquals(LocalDate.of(2026, 5, 10), result);
+ }
+
+ @Test
+ public void shouldUpdateLastRunDate() {
+
+ String jobName = "CERT_EXPIRY_REMINDER";
+ LocalDate date = LocalDate.of(2026, 5, 11);
+
+ when(jdbcTemplate.update(anyString(), any(Map.class)))
+ .thenReturn(1);
+
+ jobExecutionDao.updateLastRunDate(jobName, date);
+
+ verify(jdbcTemplate).update(
+ anyString(),
+ argThat((Map params) -> jobName.equals(params.get("jobName")) &&
+ java.sql.Date.valueOf(date).equals(params.get("lastRunDate"))));
+
+ }
+
+ @Test
+ public void shouldExecuteCorrectSqlForUpdate() {
+
+ ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class);
+
+ when(jdbcTemplate.update(sqlCaptor.capture(), any(Map.class)))
+ .thenReturn(1);
+
+ jobExecutionDao.updateLastRunDate(
+ "CERT_EXPIRY_REMINDER",
+ LocalDate.now());
+
+ String executedSql = sqlCaptor.getValue();
+
+ assertTrue(executedSql.contains("INSERT INTO job_execution_tracker"));
+ }
+
+ @Test
+ public void shouldStillExecuteUpdateEvenIfZeroRowsAffected() {
+
+ when(jdbcTemplate.update(anyString(), any(Map.class)))
+ .thenReturn(0);
+
+ jobExecutionDao.updateLastRunDate(
+ "CERT_EXPIRY_REMINDER",
+ LocalDate.now());
+
+ verify(jdbcTemplate).update(anyString(), any(Map.class));
+ }
+
+}
diff --git a/usefulStuff/sql/create_openca_ddl.sql b/usefulStuff/sql/create_openca_ddl.sql
index 0cd4bc8..0bef3ef 100644
--- a/usefulStuff/sql/create_openca_ddl.sql
+++ b/usefulStuff/sql/create_openca_ddl.sql
@@ -327,6 +327,16 @@ CREATE TABLE role_change_request (
);
+--
+-- Name: job_execution_tracker; Type: TABLE; Schema: public; Owner: -; Tablespace:
+--
+
+CREATE TABLE job_execution_tracker (
+ job_name VARCHAR(100) PRIMARY KEY,
+ last_run_date DATE
+);
+
+
--
-- Name: seq_bulk; Type: SEQUENCE; Schema: public; Owner: -
--
From 048587effa44ecca99a508bdfbb7f52567631aa6 Mon Sep 17 00:00:00 2001
From: Manoj Garai
Date: Tue, 12 May 2026 16:32:51 +0100
Subject: [PATCH 05/10] Update java doc for sendEmailReminderToUserOnCertExpiry
---
src/main/java/uk/ac/ngs/service/email/EmailService.java | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/main/java/uk/ac/ngs/service/email/EmailService.java b/src/main/java/uk/ac/ngs/service/email/EmailService.java
index bb69c01..896bd23 100644
--- a/src/main/java/uk/ac/ngs/service/email/EmailService.java
+++ b/src/main/java/uk/ac/ngs/service/email/EmailService.java
@@ -497,11 +497,8 @@ public void sendEmailOnRaopRoleRequestRejection(String recipient, String actorCN
* Sends an email reminder to the user informing them of an upcoming
* certificate expiry.
*
- * @param certKey certificate identifier
+ * @param cert certificate
* @param daysToExpire number of days remaining before expiry
- * @param cn certificate common name
- * @param dn certificate distinguished name
- * @param recipientEmail recipient's email address
*/
public void sendEmailReminderToUserOnCertExpiry(CertificateRow cert, int daysToExpire) {
From 2bcee65331c338436f7d0ab816eaf81d36de88ac Mon Sep 17 00:00:00 2001
From: Manoj Garai
Date: Wed, 13 May 2026 08:53:52 +0100
Subject: [PATCH 06/10] Add comment
---
src/main/java/uk/ac/ngs/dao/JobExecutionDao.java | 3 ++-
src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java b/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java
index c31c514..d5a21fe 100644
--- a/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java
+++ b/src/main/java/uk/ac/ngs/dao/JobExecutionDao.java
@@ -56,7 +56,8 @@ public LocalDate getLastRunDate(String jobName) {
}
public void updateLastRunDate(String jobName, LocalDate lastRunDate) {
-
+ // This query inserts a new job record if it doesn’t exist,
+ // or updates its last run date if it already exists, using PostgreSQL’s UPSERT mechanism.
String sql = """
INSERT INTO job_execution_tracker (job_name, last_run_date)
VALUES (:jobName, :lastRunDate)
diff --git a/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java b/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java
index 3e62186..039302a 100644
--- a/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java
+++ b/src/test/java/uk/ac/ngs/dao/JobExecutionDaoTests.java
@@ -58,7 +58,6 @@ public void shouldReturnLastRunDate() {
anyString(),
argThat((Map params) -> jobName.equals(params.get("jobName"))),
ArgumentMatchers.>any());
-
}
@Test
@@ -124,6 +123,7 @@ public void shouldExecuteCorrectSqlForUpdate() {
String executedSql = sqlCaptor.getValue();
assertTrue(executedSql.contains("INSERT INTO job_execution_tracker"));
+ assertTrue(executedSql.contains("ON CONFLICT"));
}
@Test
From cccf4c5ee78124766de06f11927b356c570b97e0 Mon Sep 17 00:00:00 2001
From: garaimanoj <99975605+garaimanoj@users.noreply.github.com>
Date: Wed, 13 May 2026 16:32:17 +0100
Subject: [PATCH 07/10] Update
src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
Co-authored-by: rowan04
---
.../freemarker/email/emailUserCertExpiryReminderTemplate.html | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html b/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
index f0d5133..900963d 100644
--- a/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
+++ b/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
@@ -7,8 +7,7 @@
You have a certificate which will expire in ${daysToExpire} days. If you do not wish to renew this
- certificates or if you have
- done so already then please ignore this message.
+ certificate, or if you have renewed already, please ignore this message.
Your certificate was issued for "${cn}" and has the subject:
From e5828e990dc5c25ef61c46f60f57dc7da2beacad Mon Sep 17 00:00:00 2001
From: garaimanoj <99975605+garaimanoj@users.noreply.github.com>
Date: Wed, 13 May 2026 16:32:38 +0100
Subject: [PATCH 08/10] Update
src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
Co-authored-by: rowan04
---
.../freemarker/email/emailUserCertExpiryReminderTemplate.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html b/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
index 900963d..9d09d43 100644
--- a/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
+++ b/src/main/webapp/WEB-INF/freemarker/email/emailUserCertExpiryReminderTemplate.html
@@ -17,7 +17,7 @@
If you prefer a browser interface for your certificate management and you have your current certificate in your
- browser then please go to the CA Portal ${basePortalUrl} and follow the instructions
+ browser, please go to the CA Portal ${basePortalUrl} and follow the instructions
for renewal.
From 3eb7f63c6b88d35c0798401ea64b974f06bd4c85 Mon Sep 17 00:00:00 2001
From: Manoj Garai
Date: Tue, 19 May 2026 15:37:23 +0100
Subject: [PATCH 09/10] Add reminder summary log
---
.../uk/ac/ngs/service/CertificateService.java | 20 +++++++++++++------
.../uk/ac/ngs/service/email/EmailService.java | 7 +++++--
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/src/main/java/uk/ac/ngs/service/CertificateService.java b/src/main/java/uk/ac/ngs/service/CertificateService.java
index c60c7d3..272fdcd 100644
--- a/src/main/java/uk/ac/ngs/service/CertificateService.java
+++ b/src/main/java/uk/ac/ngs/service/CertificateService.java
@@ -292,20 +292,28 @@ private void sendRemindersForDaysToExpire(LocalDate processingDate, int daysToEx
List certificates = jdbcCertDao.getValidCertificatesExpiringInDays(processingDate, daysToExpire);
- int count = certificates.size();
+ int totalCertificates = certificates.size();
- if (count > 0) {
- log.info(count + " certificate(s) are going to expire after " + daysToExpire + " days.");
+ if (totalCertificates > 0) {
+ log.info(totalCertificates + " certificate(s) are going to expire after " + daysToExpire + " days.");
} else {
log.info("No certificates are going to expire after " + daysToExpire + " days.");
}
+ int successCount = 0;
+ int failureCount = 0;
+
for (CertificateRow cert : certificates) {
log.info("Sending " + daysToExpire + "-days expiry reminder for cert [" + cert.getCert_key() + "] to "
+ cert.getEmail());
-
- this.emailService.sendEmailReminderToUserOnCertExpiry(cert, daysToExpire);
- }
+ if(this.emailService.sendEmailReminderToUserOnCertExpiry(cert, daysToExpire)){
+ successCount++;
+ } else{
+ failureCount++;
+ }
+ }
+ log.info("Summary for " + daysToExpire + "-day expiry reminder (processingDate=" + processingDate + "): total="
+ + totalCertificates + ", sent=" + successCount + ", failed=" + failureCount);
}
diff --git a/src/main/java/uk/ac/ngs/service/email/EmailService.java b/src/main/java/uk/ac/ngs/service/email/EmailService.java
index 896bd23..8e2c70e 100644
--- a/src/main/java/uk/ac/ngs/service/email/EmailService.java
+++ b/src/main/java/uk/ac/ngs/service/email/EmailService.java
@@ -501,7 +501,7 @@ public void sendEmailOnRaopRoleRequestRejection(String recipient, String actorCN
* @param daysToExpire number of days remaining before expiry
*/
- public void sendEmailReminderToUserOnCertExpiry(CertificateRow cert, int daysToExpire) {
+ public boolean sendEmailReminderToUserOnCertExpiry(CertificateRow cert, int daysToExpire) {
SimpleMailMessage msg = new SimpleMailMessage(this.emailTemplate);
msg.setTo(cert.getEmail());
msg.setSubject("Your e-Science User Certificate will expire in " + daysToExpire + " days!");
@@ -515,8 +515,11 @@ public void sendEmailReminderToUserOnCertExpiry(CertificateRow cert, int daysToE
try {
this.mailSender.send(msg, vars, this.emailUserCertExpiryReminderTemplate);
+ log.debug("Certificate expiry reminder email sent to " + cert.getCn());
+ return true;
} catch (MailException ex) {
- log.error("MailSender " + ex.getMessage());
+ log.error("Error while sending certificate expiry reminder email to " + cert.getCn() + ": " + ex.getMessage());
+ return false;
}
}
From 460828415eab8aa165da9cf41534c2ee6fc4baed Mon Sep 17 00:00:00 2001
From: Manoj Garai
Date: Wed, 20 May 2026 08:24:54 +0100
Subject: [PATCH 10/10] Format code
---
.../java/uk/ac/ngs/service/CertificateService.java | 13 +++++++------
.../java/uk/ac/ngs/service/email/EmailService.java | 3 ++-
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/main/java/uk/ac/ngs/service/CertificateService.java b/src/main/java/uk/ac/ngs/service/CertificateService.java
index 272fdcd..dab9ccb 100644
--- a/src/main/java/uk/ac/ngs/service/CertificateService.java
+++ b/src/main/java/uk/ac/ngs/service/CertificateService.java
@@ -290,8 +290,9 @@ public void sendCertificateExpiryReminders() {
private void sendRemindersForDaysToExpire(LocalDate processingDate, int daysToExpire) {
- List certificates = jdbcCertDao.getValidCertificatesExpiringInDays(processingDate, daysToExpire);
-
+ List certificates = jdbcCertDao.getValidCertificatesExpiringInDays(processingDate,
+ daysToExpire);
+
int totalCertificates = certificates.size();
if (totalCertificates > 0) {
@@ -306,12 +307,12 @@ private void sendRemindersForDaysToExpire(LocalDate processingDate, int daysToEx
for (CertificateRow cert : certificates) {
log.info("Sending " + daysToExpire + "-days expiry reminder for cert [" + cert.getCert_key() + "] to "
+ cert.getEmail());
- if(this.emailService.sendEmailReminderToUserOnCertExpiry(cert, daysToExpire)){
+ if (this.emailService.sendEmailReminderToUserOnCertExpiry(cert, daysToExpire)) {
successCount++;
- } else{
+ } else {
failureCount++;
- }
- }
+ }
+ }
log.info("Summary for " + daysToExpire + "-day expiry reminder (processingDate=" + processingDate + "): total="
+ totalCertificates + ", sent=" + successCount + ", failed=" + failureCount);
}
diff --git a/src/main/java/uk/ac/ngs/service/email/EmailService.java b/src/main/java/uk/ac/ngs/service/email/EmailService.java
index 8e2c70e..3e9729a 100644
--- a/src/main/java/uk/ac/ngs/service/email/EmailService.java
+++ b/src/main/java/uk/ac/ngs/service/email/EmailService.java
@@ -518,7 +518,8 @@ public boolean sendEmailReminderToUserOnCertExpiry(CertificateRow cert, int days
log.debug("Certificate expiry reminder email sent to " + cert.getCn());
return true;
} catch (MailException ex) {
- log.error("Error while sending certificate expiry reminder email to " + cert.getCn() + ": " + ex.getMessage());
+ log.error("Error while sending certificate expiry reminder email to " + cert.getCn() + ": "
+ + ex.getMessage());
return false;
}
}