You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2026/01/19 02:57:00 UTC
(james-project) 02/02: JAMES-4164 VacationMailet: add support for replyMode
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit aef61d6fc9655698a8bff1521398ff110388e2d1
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Wed Jan 14 15:49:35 2026 +0100
JAMES-4164 VacationMailet: add support for replyMode
---
docs/modules/servers/partials/VacationMailet.adoc | 7 ++-
.../james/transport/mailets/VacationMailet.java | 58 ++++++++++++++++++----
.../transport/mailets/VacationMailetTest.java | 29 ++++++++++-
3 files changed, 82 insertions(+), 12 deletions(-)
diff --git a/docs/modules/servers/partials/VacationMailet.adoc b/docs/modules/servers/partials/VacationMailet.adoc
index d02964c152..ea677318a7 100644
--- a/docs/modules/servers/partials/VacationMailet.adoc
+++ b/docs/modules/servers/partials/VacationMailet.adoc
@@ -3,4 +3,9 @@
This mailet uses https://jmap.io/spec-mail.html#vacation-response[JMAP VacationResponse] and
sends back a vacation notice to the sender if needed.
-The `useUserAsMailFrom` property can be set to true to use the user as the transport sender instead of `MAIL FROM: <>`.
\ No newline at end of file
+The `useUserAsMailFrom` property can be set to true to use the user as the transport sender instead of `MAIL FROM: <>`.
+
+The `replyMode` property determine how we compute the recipient of the vacation reply:
+ - `replyToHeader` (default mode) we lookup the `Reply-To` field of the incoming email
+ - `envelope` (legacy, here to enable backward behavioural compatibility) use the MAIL FROM of the original mail, which could
+not be faked but suffers in case of redirections, forwards and sender address rewrites.
\ No newline at end of file
diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/VacationMailet.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/VacationMailet.java
index 7c53462743..1c0bcad865 100644
--- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/VacationMailet.java
+++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/VacationMailet.java
@@ -20,9 +20,11 @@
package org.apache.james.transport.mailets;
import java.time.ZonedDateTime;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Optional;
+import java.util.stream.Stream;
import jakarta.inject.Inject;
import jakarta.mail.Address;
@@ -55,6 +57,23 @@ import reactor.core.publisher.Mono;
public class VacationMailet extends GenericMailet {
+ enum ReplyMode {
+ ENVELOPE("envelope"),
+ REPLY_TO_HEADER("replyToHeader");
+
+ public static Optional<ReplyMode> parse(String value) {
+ return Arrays.stream(ReplyMode.values())
+ .filter(replyMode -> replyMode.value.equalsIgnoreCase(value))
+ .findFirst();
+ }
+
+ private final String value;
+
+ ReplyMode(String value) {
+ this.value = value;
+ }
+ }
+
private static final Logger LOGGER = LoggerFactory.getLogger(VacationMailet.class);
private final VacationService vacationService;
@@ -62,6 +81,7 @@ public class VacationMailet extends GenericMailet {
private final AutomaticallySentMailDetector automaticallySentMailDetector;
private final MimeMessageBodyGenerator mimeMessageBodyGenerator;
private boolean useUserAsMailFrom = false;
+ private ReplyMode replyMode = ReplyMode.REPLY_TO_HEADER;
@Inject
public VacationMailet(VacationService vacationService, ZonedDateTimeProvider zonedDateTimeProvider,
@@ -85,7 +105,7 @@ public class VacationMailet extends GenericMailet {
if (!automaticallySentMailDetector.isAutomaticallySent(mail) && hasReplyToHeaderField && !isNoReplySender(mail)) {
ZonedDateTime processingDate = zonedDateTimeProvider.get();
mail.getRecipients()
- .forEach(mailAddress -> manageVacation(mailAddress, mail, processingDate));
+ .forEach(Throwing.consumer(mailAddress -> manageVacation(mailAddress, mail, processingDate)));
}
} catch (AddressException e) {
if (!e.getMessage().equals("Empty address")) {
@@ -99,18 +119,16 @@ public class VacationMailet extends GenericMailet {
@Override
public void init() throws MessagingException {
useUserAsMailFrom = MailetUtil.getInitParameter(getMailetConfig(), "useUserAsMailFrom").orElse(false);
+ replyMode = Optional.ofNullable(getInitParameter("replyMode"))
+ .map(value -> ReplyMode.parse(value).orElseThrow(() -> new IllegalArgumentException("Unsupported ReplyMode " + value)))
+ .orElse(ReplyMode.REPLY_TO_HEADER);
}
private static Address[] getReplyTo(Mail mail) throws MessagingException {
try {
return mail.getMessage().getReplyTo();
} catch (AddressException e) {
- InternetAddress[] replyTo = StreamUtils.ofNullable(mail.getMessage().getHeader("Reply-To"))
- .map(LenientAddressParser.DEFAULT::parseAddressList)
- .flatMap(Collection::stream)
- .filter(Mailbox.class::isInstance)
- .map(Mailbox.class::cast)
- .map(Mailbox::getAddress)
+ InternetAddress[] replyTo = parseReplyToField(mail)
.map(Throwing.function(InternetAddress::new))
.toArray(InternetAddress[]::new);
@@ -125,20 +143,40 @@ public class VacationMailet extends GenericMailet {
}
}
- private void manageVacation(MailAddress recipient, Mail processedMail, ZonedDateTime processingDate) {
+ private static Stream<String> parseReplyToField(Mail mail) throws MessagingException {
+ return StreamUtils.ofNullable(mail.getMessage().getHeader("Reply-To"))
+ .map(LenientAddressParser.DEFAULT::parseAddressList)
+ .flatMap(Collection::stream)
+ .filter(Mailbox.class::isInstance)
+ .map(Mailbox.class::cast)
+ .map(Mailbox::getAddress);
+ }
+
+ private void manageVacation(MailAddress recipient, Mail processedMail, ZonedDateTime processingDate) throws MessagingException {
if (isSentToSelf(processedMail.getMaybeSender().asOptional(), recipient)) {
return;
}
- RecipientId replyRecipient = RecipientId.fromMailAddress(processedMail.getMaybeSender().get());
+ RecipientId replyRecipient = computeReplyRecipient(processedMail);
VacationInformation vacationInformation = retrieveVacationInformation(recipient, replyRecipient);
- boolean shouldSendNotification = vacationInformation.vacation.isActiveAtDate(processingDate) && !vacationInformation.alreadySent;
+ boolean shouldSendNotification = vacationInformation.vacation().isActiveAtDate(processingDate) && !vacationInformation.alreadySent;
if (shouldSendNotification) {
sendNotification(processedMail, vacationInformation);
}
}
+ private RecipientId computeReplyRecipient(Mail processedMail) throws MessagingException {
+ return switch (replyMode) {
+ case ENVELOPE -> RecipientId.fromMailAddress(processedMail.getMaybeSender().get());
+ case REPLY_TO_HEADER -> RecipientId.fromMailAddress(
+ parseReplyToField(processedMail)
+ .findFirst()
+ .map(Throwing.function(MailAddress::new))
+ .orElse(processedMail.getMaybeSender().get()));
+ };
+ }
+
record VacationInformation(Vacation vacation, MailAddress recipient, AccountId accountId, RecipientId replyRecipient, Boolean alreadySent) {
}
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/VacationMailetTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/VacationMailetTest.java
index 16709383ab..ccb79950c8 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/VacationMailetTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/VacationMailetTest.java
@@ -161,7 +161,34 @@ public class VacationMailetTest {
.thenReturn(Mono.just(VACATION));
when(zonedDateTimeProvider.get()).thenReturn(DATE_TIME_2017);
when(automaticallySentMailDetector.isAutomaticallySent(mail)).thenReturn(false);
- when(vacationService.isNotificationRegistered(ACCOUNT_ID, recipientId)).thenReturn(Mono.just(false));
+ when(vacationService.isNotificationRegistered(any(), any())).thenReturn(Mono.just(false));
+
+ testee.service(FakeMail.builder()
+ .name("name")
+ .mimeMessage(
+ MimeMessageUtil.mimeMessageFromStream(ClassLoader.getSystemResourceAsStream("brokenReplyTo.eml")))
+ .sender(originalSender)
+ .recipient(originalRecipient)
+ .build());
+
+ verify(mailetContext).sendMail(eq(MailAddress.nullSender()), eq(ImmutableList.of(new MailAddress("invalid@domain.com"))), any());
+ verifyNoMoreInteractions(mailetContext);
+ }
+
+ @Test
+ public void shouldSendNotificationToMailFromWhenEnvelopeReplyMode() throws Exception {
+ when(vacationService.retrieveVacation(AccountId.fromString(USERNAME)))
+ .thenReturn(Mono.just(VACATION));
+ when(zonedDateTimeProvider.get()).thenReturn(DATE_TIME_2017);
+ when(automaticallySentMailDetector.isAutomaticallySent(mail)).thenReturn(false);
+ when(vacationService.isNotificationRegistered(any(), any())).thenReturn(Mono.just(false));
+
+
+ testee.init(FakeMailetConfig.builder()
+ .mailetName("vacation")
+ .mailetContext(mailetContext)
+ .setProperty("replyMode", "envelope")
+ .build());
testee.service(FakeMail.builder()
.name("name")
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org