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 <[email protected]>
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("[email protected]"))), 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: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to