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> paramsCaptor = ArgumentCaptor.forClass(Map.class); + + when(jdbcTemplate.query( + eq(EXPECTED_SQL), + paramsCaptor.capture(), + ArgumentMatchers.>any())) + .thenReturn(List.of()); + + // when + jdbcCertificateDao.getValidCertificatesExpiringInDays(7); + + // then + Map params = paramsCaptor.getValue(); + + assertTrue(params.containsKey("startOfDay")); + assertTrue(params.containsKey("endOfDay")); + + Object startObj = params.get("startOfDay"); + Object endObj = params.get("endOfDay"); + + assertTrue(startObj instanceof Long); + assertTrue(endObj instanceof Long); + + long start = (Long) startObj; + long end = (Long) endObj; + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + LocalDateTime startDateTime = LocalDateTime.parse(String.valueOf(start), formatter); + + LocalDateTime endDateTime = LocalDateTime.parse(String.valueOf(end), formatter); + + // Sanity checks + assertTrue(startDateTime.isBefore(endDateTime)); + + // Exactly 1 day window + assertEquals( + Duration.ofDays(1), + Duration.between(startDateTime, endDateTime)); + + } } From 7068a1fe3dceef5532d247eac0f4de54c983e4f8 Mon Sep 17 00:00:00 2001 From: Manoj Garai Date: Mon, 27 Apr 2026 16:09:22 +0100 Subject: [PATCH 02/10] Add email subject --- src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java | 4 +++- src/main/java/uk/ac/ngs/service/email/EmailService.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java b/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java index 9c7b1eb..50682e6 100644 --- a/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java +++ b/src/main/java/uk/ac/ngs/dao/JdbcCertificateDao.java @@ -481,9 +481,11 @@ public List getValidCertificatesExpiringInDays(int daysToExpire) LocalDate targetDate = LocalDate.now(ZoneOffset.UTC).plusDays(daysToExpire); + // yyyyMMddHHmmss produces only digits → safe to parse as long long startOfDay = Long.parseLong( formatter.format(targetDate.atStartOfDay(ZoneOffset.UTC))); - + + // yyyyMMddHHmmss produces only digits → safe to parse as long long endOfDay = Long.parseLong( formatter.format(targetDate.plusDays(1) .atStartOfDay(ZoneOffset.UTC))); 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 80165e5..fca89d6 100644 --- a/src/main/java/uk/ac/ngs/service/email/EmailService.java +++ b/src/main/java/uk/ac/ngs/service/email/EmailService.java @@ -505,6 +505,7 @@ public void sendEmailOnRaopRoleRequestRejection(String recipient, String actorCN public void sendEmailReminderToUserOnCertExpiry(long certKey, int daysToExpire, String cn, String dn, String recipientEmail) { SimpleMailMessage msg = new SimpleMailMessage(this.emailTemplate); msg.setTo(recipientEmail); + msg.setSubject("Your e-Science User Certificate will expire in " + daysToExpire + " days!"); Map vars = new HashMap<>(); vars.put("certKey", certKey); From 74dbe7eac7601e67abb7a1bd64d16e83dde28416 Mon Sep 17 00:00:00 2001 From: Manoj Garai Date: Mon, 11 May 2026 15:37:46 +0100 Subject: [PATCH 03/10] Add tests and get schedule time from property file --- src/main/java/uk/ac/ngs/TaskConfig.java | 3 +- .../uk/ac/ngs/service/CertificateService.java | 8 +- .../uk/ac/ngs/service/email/EmailService.java | 12 ++- .../resources/application.propertiesTEMPLATE | 3 + .../ngs/service/email/EmailServiceTests.java | 92 +++++++++++++++++++ 5 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 src/test/java/uk/ac/ngs/service/email/EmailServiceTests.java diff --git a/src/main/java/uk/ac/ngs/TaskConfig.java b/src/main/java/uk/ac/ngs/TaskConfig.java index 96240f3..9b41629 100644 --- a/src/main/java/uk/ac/ngs/TaskConfig.java +++ b/src/main/java/uk/ac/ngs/TaskConfig.java @@ -36,7 +36,8 @@ public TaskConfig(CertificateService certificateService) { this.certificateService = certificateService; } - @Scheduled(cron = "0 44 15 * * ?") + + @Scheduled(cron = "${cert.expiry.reminder.cron:0 0 7 * * ?}") public void runDailyCertificateExpiryReminderJob() { log.info("Starting daily certificate expiry reminder job"); diff --git a/src/main/java/uk/ac/ngs/service/CertificateService.java b/src/main/java/uk/ac/ngs/service/CertificateService.java index 9fc8222..c44130c 100644 --- a/src/main/java/uk/ac/ngs/service/CertificateService.java +++ b/src/main/java/uk/ac/ngs/service/CertificateService.java @@ -256,7 +256,6 @@ private void sendEmailNotificationOnRoleChangeRequest(CertificateRow targetCert, *

*/ 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; } }