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]
