This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 773d1bd80 fix(alert): parse quoted email recipient lists correctly
(#6663)
773d1bd80 is described below
commit 773d1bd8092fadcd220aaef5d2df66e271ba470e
Author: 健康宝 <[email protected]>
AuthorDate: Thu Oct 15 19:07:44 2026 +0800
fix(alert): parse quoted email recipient lists correctly (#6663)
---
.../ops/alert/NotificationOutboxService.java | 41 ++++++++++++--
.../ops/alert/NotificationOutboxServiceTest.java | 64 ++++++++++++++++++++++
2 files changed, 99 insertions(+), 6 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
index 9a44f4560..26e8b017f 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxService.java
@@ -498,20 +498,49 @@ public class NotificationOutboxService {
private static String[] parseRecipients(String raw) throws
AddressException {
List<String> recipients = new ArrayList<>();
- for (String value : raw.split("[,;]")) {
- String recipient = value.trim();
- if (!recipient.isEmpty()) {
- InternetAddress address = new InternetAddress(recipient, true);
- address.validate();
- recipients.add(address.getAddress());
+ // Split on the documented separators (comma, plus the legacy
+ // semicolon) only outside a quoted display name: a list like
+ // `"Doe, John" <[email protected]>, [email protected]` is valid RFC 822
+ // input, but a plain [,;] split broke it into unbalanced fragments
+ // that failed validation and drove the notification into retry
+ // instead of sending.
+ StringBuilder current = new StringBuilder();
+ boolean quoted = false;
+ boolean escaped = false;
+ for (int i = 0; i < raw.length(); i++) {
+ char c = raw.charAt(i);
+ if (escaped) {
+ current.append(c);
+ escaped = false;
+ } else if (quoted && c == '\\') {
+ current.append(c);
+ escaped = true;
+ } else if (c == '"') {
+ current.append(c);
+ quoted = !quoted;
+ } else if (!quoted && (c == ',' || c == ';')) {
+ addRecipient(current, recipients);
+ current.setLength(0);
+ } else {
+ current.append(c);
}
}
+ addRecipient(current, recipients);
if (recipients.isEmpty()) {
throw new AddressException("No valid email recipients");
}
return recipients.toArray(String[]::new);
}
+ private static void addRecipient(CharSequence token, List<String>
recipients) throws AddressException {
+ String recipient = token.toString().trim();
+ if (!recipient.isEmpty()) {
+ InternetAddress address = new InternetAddress(recipient, true);
+ address.validate();
+ recipients.add(address.getAddress());
+ }
+ }
+
private Map<String, Object> payload(SystemAlertVO alert, String channel,
String content) {
if ("dingtalk".equals(channel)) {
return Map.of("msgtype", "text", "text", Map.of("content",
content));
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
index e17a2da23..37fa0bcf8 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/NotificationOutboxServiceTest.java
@@ -466,6 +466,70 @@ class NotificationOutboxServiceTest {
"alertId=9, channel=email", "SUCCESS", null);
}
+ @Test
+ void dispatchesEmailWithQuotedDisplayNameRecipientsTest() {
+ RmqAlertNotificationOutboxMapper mapper =
mock(RmqAlertNotificationOutboxMapper.class);
+ SettingsRepository settings = mock(SettingsRepository.class);
+ AlertRepository alerts = mock(AlertRepository.class);
+ OperationAuditService audit = mock(OperationAuditService.class);
+ JavaMailSender mailSender = mock(JavaMailSender.class);
+ RmqAlertNotificationOutbox row = new RmqAlertNotificationOutbox();
+ row.setId(18L);
+ row.setAlertId(9L);
+ row.setChannel("email");
+ row.setStatus("PENDING");
+ row.setAttemptCount(0);
+ when(mapper.findDispatchable(any(LocalDateTime.class),
any(LocalDateTime.class), any(Integer.class)))
+ .thenReturn(List.of(row));
+ when(mapper.claimForDispatch(any(), any(LocalDateTime.class),
any(LocalDateTime.class),
+ any(LocalDateTime.class), anyString())).thenReturn(1);
+ when(mapper.update(any(), any())).thenReturn(1);
+
when(alerts.findAlertById(9L)).thenReturn(Optional.of(SystemAlertVO.builder().id(9L)
+
.level(AlertLevel.warning).title("Lag").description("high").instanceId("local").build()));
+
when(settings.loadGeneralSettings()).thenReturn(GeneralSettingsVO.builder()
+ .emailRecipients("\"Doe, John\" <[email protected]>,
[email protected]").build());
+
+ new NotificationOutboxService(mapper, settings,
mock(AlertSilenceService.class), alerts, audit,
+ new RestTemplate(), () -> mailSender).dispatch();
+
+ // A comma inside a quoted display name is not a separator: the
+ // delivery must reach both configured recipients, not fail parsing.
+ verify(mailSender).send(argThat((SimpleMailMessage message) ->
message.getTo() != null
+ && Arrays.equals(message.getTo(), new String[]
{"[email protected]", "[email protected]"})));
+ }
+
+ @Test
+ void dispatchesEmailToSemicolonSeparatedRecipientsTest() {
+ RmqAlertNotificationOutboxMapper mapper =
mock(RmqAlertNotificationOutboxMapper.class);
+ SettingsRepository settings = mock(SettingsRepository.class);
+ AlertRepository alerts = mock(AlertRepository.class);
+ OperationAuditService audit = mock(OperationAuditService.class);
+ JavaMailSender mailSender = mock(JavaMailSender.class);
+ RmqAlertNotificationOutbox row = new RmqAlertNotificationOutbox();
+ row.setId(19L);
+ row.setAlertId(9L);
+ row.setChannel("email");
+ row.setStatus("PENDING");
+ row.setAttemptCount(0);
+ when(mapper.findDispatchable(any(LocalDateTime.class),
any(LocalDateTime.class), any(Integer.class)))
+ .thenReturn(List.of(row));
+ when(mapper.claimForDispatch(any(), any(LocalDateTime.class),
any(LocalDateTime.class),
+ any(LocalDateTime.class), anyString())).thenReturn(1);
+ when(mapper.update(any(), any())).thenReturn(1);
+
when(alerts.findAlertById(9L)).thenReturn(Optional.of(SystemAlertVO.builder().id(9L)
+
.level(AlertLevel.warning).title("Lag").description("high").instanceId("local").build()));
+
when(settings.loadGeneralSettings()).thenReturn(GeneralSettingsVO.builder()
+ .emailRecipients("[email protected]; \"Night; Shift\"
<[email protected]>").build());
+
+ new NotificationOutboxService(mapper, settings,
mock(AlertSilenceService.class), alerts, audit,
+ new RestTemplate(), () -> mailSender).dispatch();
+
+ // The legacy semicolon separator keeps working, including inside a
+ // quoted display name.
+ verify(mailSender).send(argThat((SimpleMailMessage message) ->
message.getTo() != null
+ && Arrays.equals(message.getTo(), new String[]
{"[email protected]", "[email protected]"})));
+ }
+
@Test
void reclaimsAStaleSendingDeliveryTest() {
RmqAlertNotificationOutboxMapper mapper =
mock(RmqAlertNotificationOutboxMapper.class);