Skip to content

Commit aef61d6

Browse files
chibenwaArsnael
authored andcommitted
JAMES-4164 VacationMailet: add support for replyMode
1 parent 7641b10 commit aef61d6

3 files changed

Lines changed: 82 additions & 12 deletions

File tree

docs/modules/servers/partials/VacationMailet.adoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@
33
This mailet uses https://jmap.io/spec-mail.html#vacation-response[JMAP VacationResponse] and
44
sends back a vacation notice to the sender if needed.
55

6-
The `useUserAsMailFrom` property can be set to true to use the user as the transport sender instead of `MAIL FROM: <>`.
6+
The `useUserAsMailFrom` property can be set to true to use the user as the transport sender instead of `MAIL FROM: <>`.
7+
8+
The `replyMode` property determine how we compute the recipient of the vacation reply:
9+
- `replyToHeader` (default mode) we lookup the `Reply-To` field of the incoming email
10+
- `envelope` (legacy, here to enable backward behavioural compatibility) use the MAIL FROM of the original mail, which could
11+
not be faked but suffers in case of redirections, forwards and sender address rewrites.

server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/VacationMailet.java

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
package org.apache.james.transport.mailets;
2121

2222
import java.time.ZonedDateTime;
23+
import java.util.Arrays;
2324
import java.util.Collection;
2425
import java.util.Locale;
2526
import java.util.Optional;
27+
import java.util.stream.Stream;
2628

2729
import jakarta.inject.Inject;
2830
import jakarta.mail.Address;
@@ -55,13 +57,31 @@
5557

5658
public class VacationMailet extends GenericMailet {
5759

60+
enum ReplyMode {
61+
ENVELOPE("envelope"),
62+
REPLY_TO_HEADER("replyToHeader");
63+
64+
public static Optional<ReplyMode> parse(String value) {
65+
return Arrays.stream(ReplyMode.values())
66+
.filter(replyMode -> replyMode.value.equalsIgnoreCase(value))
67+
.findFirst();
68+
}
69+
70+
private final String value;
71+
72+
ReplyMode(String value) {
73+
this.value = value;
74+
}
75+
}
76+
5877
private static final Logger LOGGER = LoggerFactory.getLogger(VacationMailet.class);
5978

6079
private final VacationService vacationService;
6180
private final ZonedDateTimeProvider zonedDateTimeProvider;
6281
private final AutomaticallySentMailDetector automaticallySentMailDetector;
6382
private final MimeMessageBodyGenerator mimeMessageBodyGenerator;
6483
private boolean useUserAsMailFrom = false;
84+
private ReplyMode replyMode = ReplyMode.REPLY_TO_HEADER;
6585

6686
@Inject
6787
public VacationMailet(VacationService vacationService, ZonedDateTimeProvider zonedDateTimeProvider,
@@ -85,7 +105,7 @@ public void service(Mail mail) {
85105
if (!automaticallySentMailDetector.isAutomaticallySent(mail) && hasReplyToHeaderField && !isNoReplySender(mail)) {
86106
ZonedDateTime processingDate = zonedDateTimeProvider.get();
87107
mail.getRecipients()
88-
.forEach(mailAddress -> manageVacation(mailAddress, mail, processingDate));
108+
.forEach(Throwing.consumer(mailAddress -> manageVacation(mailAddress, mail, processingDate)));
89109
}
90110
} catch (AddressException e) {
91111
if (!e.getMessage().equals("Empty address")) {
@@ -99,18 +119,16 @@ public void service(Mail mail) {
99119
@Override
100120
public void init() throws MessagingException {
101121
useUserAsMailFrom = MailetUtil.getInitParameter(getMailetConfig(), "useUserAsMailFrom").orElse(false);
122+
replyMode = Optional.ofNullable(getInitParameter("replyMode"))
123+
.map(value -> ReplyMode.parse(value).orElseThrow(() -> new IllegalArgumentException("Unsupported ReplyMode " + value)))
124+
.orElse(ReplyMode.REPLY_TO_HEADER);
102125
}
103126

104127
private static Address[] getReplyTo(Mail mail) throws MessagingException {
105128
try {
106129
return mail.getMessage().getReplyTo();
107130
} catch (AddressException e) {
108-
InternetAddress[] replyTo = StreamUtils.ofNullable(mail.getMessage().getHeader("Reply-To"))
109-
.map(LenientAddressParser.DEFAULT::parseAddressList)
110-
.flatMap(Collection::stream)
111-
.filter(Mailbox.class::isInstance)
112-
.map(Mailbox.class::cast)
113-
.map(Mailbox::getAddress)
131+
InternetAddress[] replyTo = parseReplyToField(mail)
114132
.map(Throwing.function(InternetAddress::new))
115133
.toArray(InternetAddress[]::new);
116134

@@ -125,20 +143,40 @@ private static Address[] getReplyTo(Mail mail) throws MessagingException {
125143
}
126144
}
127145

128-
private void manageVacation(MailAddress recipient, Mail processedMail, ZonedDateTime processingDate) {
146+
private static Stream<String> parseReplyToField(Mail mail) throws MessagingException {
147+
return StreamUtils.ofNullable(mail.getMessage().getHeader("Reply-To"))
148+
.map(LenientAddressParser.DEFAULT::parseAddressList)
149+
.flatMap(Collection::stream)
150+
.filter(Mailbox.class::isInstance)
151+
.map(Mailbox.class::cast)
152+
.map(Mailbox::getAddress);
153+
}
154+
155+
private void manageVacation(MailAddress recipient, Mail processedMail, ZonedDateTime processingDate) throws MessagingException {
129156
if (isSentToSelf(processedMail.getMaybeSender().asOptional(), recipient)) {
130157
return;
131158
}
132159

133-
RecipientId replyRecipient = RecipientId.fromMailAddress(processedMail.getMaybeSender().get());
160+
RecipientId replyRecipient = computeReplyRecipient(processedMail);
134161
VacationInformation vacationInformation = retrieveVacationInformation(recipient, replyRecipient);
135162

136-
boolean shouldSendNotification = vacationInformation.vacation.isActiveAtDate(processingDate) && !vacationInformation.alreadySent;
163+
boolean shouldSendNotification = vacationInformation.vacation().isActiveAtDate(processingDate) && !vacationInformation.alreadySent;
137164
if (shouldSendNotification) {
138165
sendNotification(processedMail, vacationInformation);
139166
}
140167
}
141168

169+
private RecipientId computeReplyRecipient(Mail processedMail) throws MessagingException {
170+
return switch (replyMode) {
171+
case ENVELOPE -> RecipientId.fromMailAddress(processedMail.getMaybeSender().get());
172+
case REPLY_TO_HEADER -> RecipientId.fromMailAddress(
173+
parseReplyToField(processedMail)
174+
.findFirst()
175+
.map(Throwing.function(MailAddress::new))
176+
.orElse(processedMail.getMaybeSender().get()));
177+
};
178+
}
179+
142180
record VacationInformation(Vacation vacation, MailAddress recipient, AccountId accountId, RecipientId replyRecipient, Boolean alreadySent) {
143181

144182
}

server/mailet/mailets/src/test/java/org/apache/james/transport/mailets/VacationMailetTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,34 @@ public void shouldSendNotificationWhenBrokenReplyTo() throws Exception {
161161
.thenReturn(Mono.just(VACATION));
162162
when(zonedDateTimeProvider.get()).thenReturn(DATE_TIME_2017);
163163
when(automaticallySentMailDetector.isAutomaticallySent(mail)).thenReturn(false);
164-
when(vacationService.isNotificationRegistered(ACCOUNT_ID, recipientId)).thenReturn(Mono.just(false));
164+
when(vacationService.isNotificationRegistered(any(), any())).thenReturn(Mono.just(false));
165+
166+
testee.service(FakeMail.builder()
167+
.name("name")
168+
.mimeMessage(
169+
MimeMessageUtil.mimeMessageFromStream(ClassLoader.getSystemResourceAsStream("brokenReplyTo.eml")))
170+
.sender(originalSender)
171+
.recipient(originalRecipient)
172+
.build());
173+
174+
verify(mailetContext).sendMail(eq(MailAddress.nullSender()), eq(ImmutableList.of(new MailAddress("invalid@domain.com"))), any());
175+
verifyNoMoreInteractions(mailetContext);
176+
}
177+
178+
@Test
179+
public void shouldSendNotificationToMailFromWhenEnvelopeReplyMode() throws Exception {
180+
when(vacationService.retrieveVacation(AccountId.fromString(USERNAME)))
181+
.thenReturn(Mono.just(VACATION));
182+
when(zonedDateTimeProvider.get()).thenReturn(DATE_TIME_2017);
183+
when(automaticallySentMailDetector.isAutomaticallySent(mail)).thenReturn(false);
184+
when(vacationService.isNotificationRegistered(any(), any())).thenReturn(Mono.just(false));
185+
186+
187+
testee.init(FakeMailetConfig.builder()
188+
.mailetName("vacation")
189+
.mailetContext(mailetContext)
190+
.setProperty("replyMode", "envelope")
191+
.build());
165192

166193
testee.service(FakeMail.builder()
167194
.name("name")

0 commit comments

Comments
 (0)