You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by rc...@apache.org on 2020/02/13 02:29:17 UTC
[james-project] 06/21: JAMES-3032 allow user to send an email with
a from address containing one of her alias
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 fafe4ee1d7d0eed289a20d70d3a51a0ab4439bb6
Author: RĂ©mi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Jan 30 15:12:20 2020 +0100
JAMES-3032 allow user to send an email with a from address containing one of her alias
Currently James checks that the user connected matches the user is the From header of a mail being sent.
Instead, James should allow that the From header contains any alias of the connected user.
This also matches the current JMAP specification security considerations: https://jmap.io/spec-mail.html#permission-to-send-from-an-address
---
.../james/rrt/api/RecipientRewriteTable.java | 10 +-
.../rrt/lib/AbstractRecipientRewriteTable.java | 30 ++---
.../methods/integration/SetMessagesMethodTest.java | 128 ++++++++++++++++++++-
server/protocols/jmap-draft/pom.xml | 5 +
.../methods/SetMessagesCreationProcessor.java | 33 +++---
.../methods/SetMessagesCreationProcessorTest.java | 120 ++++++++++++++++++-
.../apache/james/jmap/JMAPTestingConstants.java | 1 +
7 files changed, 293 insertions(+), 34 deletions(-)
diff --git a/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java b/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java
index 3f9b339..e3ea519 100644
--- a/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java
+++ b/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientRewriteTable.java
@@ -94,7 +94,15 @@ public interface RecipientRewriteTable {
* @throws ErrorMappingException
* get thrown if an error mapping was found
*/
- Mappings getResolvedMappings(String user, Domain domain) throws ErrorMappingException, RecipientRewriteTableException;
+ default Mappings getResolvedMappings(String user, Domain domain) throws ErrorMappingException, RecipientRewriteTableException {
+ return getResolvedMappings(user, domain, EnumSet.allOf(Mapping.Type.class));
+ }
+
+ /**
+ * Return the Mappings for the given source, only the mapping with the given mapping types are considered during the resolution.
+ * Return empty object if no matched mapping was found
+ */
+ Mappings getResolvedMappings(String user, Domain domain, EnumSet<Mapping.Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException;
/**
* Return the explicit mapping stored for the given user and domain. Return empty object
diff --git a/server/data/data-library/src/main/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTable.java b/server/data/data-library/src/main/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTable.java
index a43900f..260d5f6 100644
--- a/server/data/data-library/src/main/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTable.java
+++ b/server/data/data-library/src/main/java/org/apache/james/rrt/lib/AbstractRecipientRewriteTable.java
@@ -18,6 +18,7 @@
****************************************************************/
package org.apache.james.rrt.lib;
+import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@@ -101,11 +102,12 @@ public abstract class AbstractRecipientRewriteTable implements RecipientRewriteT
}
@Override
- public Mappings getResolvedMappings(String user, Domain domain) throws ErrorMappingException, RecipientRewriteTableException {
- return getMappings(Username.fromLocalPartWithDomain(user, domain), mappingLimit);
+ public Mappings getResolvedMappings(String user, Domain domain, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
+
+ return getMappings(Username.fromLocalPartWithDomain(user, domain), mappingLimit, mappingTypes);
}
- private Mappings getMappings(Username username, int mappingLimit) throws ErrorMappingException, RecipientRewriteTableException {
+ private Mappings getMappings(Username username, int mappingLimit, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
// We have to much mappings throw ErrorMappingException to avoid
// infinity loop
@@ -113,23 +115,25 @@ public abstract class AbstractRecipientRewriteTable implements RecipientRewriteT
throw new TooManyMappingException("554 Too many mappings to process");
}
- Mappings targetMappings = mapAddress(username.getLocalPart(), username.getDomainPart().get());
-
+ Domain domain = username.getDomainPart().get();
+ String localPart = username.getLocalPart();
+ Stream<Mapping> targetMappings = mapAddress(localPart, domain).asStream()
+ .filter(mapping -> mappingTypes.contains(mapping.getType()));
try {
return MappingsImpl.fromMappings(
- targetMappings.asStream()
- .flatMap(Throwing.function((Mapping target) -> convertAndRecurseMapping(username, target, mappingLimit)).sneakyThrow()));
+ targetMappings
+ .flatMap(Throwing.function((Mapping target) -> convertAndRecurseMapping(username, target, mappingLimit, mappingTypes)).sneakyThrow()));
} catch (SkipMappingProcessingException e) {
return MappingsImpl.empty();
}
}
- private Stream<Mapping> convertAndRecurseMapping(Username originalUsername, Mapping associatedMapping, int remainingLoops) throws ErrorMappingException, RecipientRewriteTableException, SkipMappingProcessingException, AddressException {
+ private Stream<Mapping> convertAndRecurseMapping(Username originalUsername, Mapping associatedMapping, int remainingLoops, EnumSet<Type> mappingTypes) throws ErrorMappingException, SkipMappingProcessingException, AddressException {
Function<Username, Stream<Mapping>> convertAndRecurseMapping =
Throwing
- .function((Username rewrittenUser) -> convertAndRecurseMapping(associatedMapping, originalUsername, rewrittenUser, remainingLoops))
+ .function((Username rewrittenUser) -> convertAndRecurseMapping(associatedMapping, originalUsername, rewrittenUser, remainingLoops, mappingTypes))
.sneakyThrow();
return associatedMapping.rewriteUser(originalUsername)
@@ -138,7 +142,7 @@ public abstract class AbstractRecipientRewriteTable implements RecipientRewriteT
.orElse(Stream.empty());
}
- private Stream<Mapping> convertAndRecurseMapping(Mapping mapping, Username originalUsername, Username rewrittenUsername, int remainingLoops) throws ErrorMappingException, RecipientRewriteTableException {
+ private Stream<Mapping> convertAndRecurseMapping(Mapping mapping, Username originalUsername, Username rewrittenUsername, int remainingLoops, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
LOGGER.debug("Valid virtual user mapping {} to {}", originalUsername.asString(), rewrittenUsername.asString());
Stream<Mapping> nonRecursiveResult = Stream.of(toMapping(rewrittenUsername, mapping.getType()));
@@ -150,12 +154,12 @@ public abstract class AbstractRecipientRewriteTable implements RecipientRewriteT
if (originalUsername.equals(rewrittenUsername)) {
return mapping.handleIdentity(nonRecursiveResult);
} else {
- return recurseMapping(nonRecursiveResult, rewrittenUsername, remainingLoops);
+ return recurseMapping(nonRecursiveResult, rewrittenUsername, remainingLoops, mappingTypes);
}
}
- private Stream<Mapping> recurseMapping(Stream<Mapping> nonRecursiveResult, Username targetUsername, int remainingLoops) throws ErrorMappingException, RecipientRewriteTableException {
- Mappings childMappings = getMappings(targetUsername, remainingLoops - 1);
+ private Stream<Mapping> recurseMapping(Stream<Mapping> nonRecursiveResult, Username targetUsername, int remainingLoops, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
+ Mappings childMappings = getMappings(targetUsername, remainingLoops - 1, mappingTypes);
if (childMappings.isEmpty()) {
return nonRecursiveResult;
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
index 39470b5..a5776b2 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
@@ -29,6 +29,7 @@ import static org.apache.james.jmap.JMAPTestingConstants.ARGUMENTS;
import static org.apache.james.jmap.JMAPTestingConstants.BOB;
import static org.apache.james.jmap.JMAPTestingConstants.BOB_PASSWORD;
import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN;
+import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN_ALIAS;
import static org.apache.james.jmap.JMAPTestingConstants.LOCALHOST_IP;
import static org.apache.james.jmap.JMAPTestingConstants.NAME;
import static org.apache.james.jmap.JMAPTestingConstants.SECOND_ARGUMENTS;
@@ -126,7 +127,6 @@ import org.junit.experimental.categories.Category;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
-
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.filter.log.LogDetail;
@@ -137,6 +137,9 @@ public abstract class SetMessagesMethodTest {
private static final String FORWARDED = "$Forwarded";
private static final int _1MB = 1024 * 1024;
private static final Username USERNAME = Username.of("username@" + DOMAIN);
+ private static final String ALIAS_OF_USERNAME_MAIL = "alias@" + DOMAIN;
+ private static final String GROUP_MAIL = "group@" + DOMAIN;
+ private static final Username ALIAS_OF_USERNAME = Username.of(ALIAS_OF_USERNAME_MAIL);
private static final String PASSWORD = "password";
private static final MailboxPath USER_MAILBOX = MailboxPath.forUser(USERNAME, "mailbox");
private static final String NOT_UPDATED = ARGUMENTS + ".notUpdated";
@@ -179,6 +182,7 @@ public abstract class SetMessagesMethodTest {
dataProbe.addDomain(DOMAIN);
dataProbe.addUser(USERNAME.asString(), PASSWORD);
dataProbe.addUser(BOB.asString(), BOB_PASSWORD);
+
mailboxProbe.createMailbox("#private", USERNAME.asString(), DefaultMailboxes.INBOX);
accessToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(jmapServer), USERNAME, PASSWORD);
bobAccessToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(jmapServer), BOB, BOB_PASSWORD);
@@ -2500,6 +2504,128 @@ public abstract class SetMessagesMethodTest {
}
@Test
+ public void setMessagesShouldSucceedWhenSendingMessageFromAnAliasOfTheConnectedUser() throws Exception {
+ dataProbe.addUserAliasMapping(Username.of(ALIAS_OF_USERNAME_MAIL).getLocalPart(), ALIAS_OF_USERNAME.getDomainPart().get().asString(), USERNAME.asString());
+
+ String messageCreationId = "creationId1337";
+ String requestBody = "[" +
+ " [" +
+ " \"setMessages\"," +
+ " {" +
+ " \"create\": { \"" + messageCreationId + "\" : {" +
+ " \"from\": { \"email\": \"" + ALIAS_OF_USERNAME_MAIL + "\"}," +
+ " \"to\": [{ \"name\": \"BOB\", \"email\": \"" + BOB.asString() + "\"}]," +
+ " \"subject\": \"Thank you for joining example.com!\"," +
+ " \"textBody\": \"Hello someone, and thank you for joining example.com!\"," +
+ " \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+ " }}" +
+ " }," +
+ " \"#0\"" +
+ " ]" +
+ "]";
+
+ given()
+ .header("Authorization", accessToken.asString())
+ .body(requestBody)
+ .when()
+ .post("/jmap")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200)
+ .body(NAME, equalTo("messagesSet"))
+ .body(ARGUMENTS + ".created", aMapWithSize(1))
+ .body(ARGUMENTS + ".created", hasKey(messageCreationId))
+ .body(ARGUMENTS + ".created[\"" + messageCreationId + "\"].headers.From", equalTo(ALIAS_OF_USERNAME_MAIL))
+ .body(ARGUMENTS + ".created[\"" + messageCreationId + "\"].from.name", equalTo(ALIAS_OF_USERNAME_MAIL))
+ .body(ARGUMENTS + ".created[\"" + messageCreationId + "\"].from.email", equalTo(ALIAS_OF_USERNAME_MAIL));
+
+ calmlyAwait
+ .pollDelay(Duration.FIVE_HUNDRED_MILLISECONDS)
+ .atMost(30, TimeUnit.SECONDS).until(() -> isAnyMessageFoundInRecipientsMailboxes(bobAccessToken));
+
+ }
+
+ @Test
+ public void setMessagesShouldSucceedWhenSendingMessageFromADomainAliasOfTheConnectedUser() throws Exception {
+ dataProbe.addDomain(DOMAIN_ALIAS);
+ dataProbe.addDomainAliasMapping(DOMAIN_ALIAS, DOMAIN);
+
+ String messageCreationId = "creationId1337";
+ String alias = USERNAME.getLocalPart() + "@" + DOMAIN_ALIAS;
+ String requestBody = "[" +
+ " [" +
+ " \"setMessages\"," +
+ " {" +
+ " \"create\": { \"" + messageCreationId + "\" : {" +
+ " \"from\": { \"email\": \"" + alias + "\"}," +
+ " \"to\": [{ \"name\": \"BOB\", \"email\": \"" + BOB.asString() + "\"}]," +
+ " \"subject\": \"Thank you for joining example.com!\"," +
+ " \"textBody\": \"Hello someone, and thank you for joining example.com!\"," +
+ " \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+ " }}" +
+ " }," +
+ " \"#0\"" +
+ " ]" +
+ "]";
+
+ given()
+ .header("Authorization", accessToken.asString())
+ .body(requestBody)
+ .when()
+ .post("/jmap")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200)
+ .body(NAME, equalTo("messagesSet"))
+ .body(ARGUMENTS + ".created", aMapWithSize(1))
+ .body(ARGUMENTS + ".created", hasKey(messageCreationId))
+ .body(ARGUMENTS + ".created[\"" + messageCreationId + "\"].headers.From", equalTo(alias))
+ .body(ARGUMENTS + ".created[\"" + messageCreationId + "\"].from.name", equalTo(alias))
+ .body(ARGUMENTS + ".created[\"" + messageCreationId + "\"].from.email", equalTo(alias));
+
+ calmlyAwait
+ .pollDelay(Duration.FIVE_HUNDRED_MILLISECONDS)
+ .atMost(30, TimeUnit.SECONDS).until(() -> isAnyMessageFoundInRecipientsMailboxes(bobAccessToken));
+ }
+
+ @Test
+ public void setMessagesShouldFailWhenSendingMessageFromAGroupAliasOfTheConnectedUser() throws Exception {
+ dataProbe.addGroupAliasMapping(GROUP_MAIL, USERNAME.asString());
+
+ String messageCreationId = "creationId1337";
+ String requestBody = "[" +
+ " [" +
+ " \"setMessages\"," +
+ " {" +
+ " \"create\": { \"" + messageCreationId + "\" : {" +
+ " \"from\": { \"email\": \"" + GROUP_MAIL + "\"}," +
+ " \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+ " \"subject\": \"Thank you for joining example.com!\"," +
+ " \"textBody\": \"Hello someone, and thank you for joining example.com!\"," +
+ " \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+ " }}" +
+ " }," +
+ " \"#0\"" +
+ " ]" +
+ "]";
+
+ given()
+ .header("Authorization", accessToken.asString())
+ .body(requestBody)
+ .when()
+ .post("/jmap")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200)
+ .body(ARGUMENTS + ".created", anEmptyMap())
+ .body(ARGUMENTS + ".notCreated", aMapWithSize(1))
+ .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId));
+
+ String outboxId = getMailboxId(accessToken, Role.OUTBOX);
+ assertThat(hasNoMessageIn(bobAccessToken, outboxId)).isTrue();
+ }
+
+ @Test
public void setMessagesShouldNotCreateMessageInOutboxWhenSendingMessageWithAnotherFromAddressThanTheConnectedUser() {
String messageCreationId = "creationId1337";
String requestBody = "[" +
diff --git a/server/protocols/jmap-draft/pom.xml b/server/protocols/jmap-draft/pom.xml
index 3167500..49ee302 100644
--- a/server/protocols/jmap-draft/pom.xml
+++ b/server/protocols/jmap-draft/pom.xml
@@ -99,6 +99,11 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-dnsservice-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-filesystem-api</artifactId>
<scope>test</scope>
<type>test-jar</type>
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java
index b51c831..09904d6 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java
@@ -62,6 +62,7 @@ import org.apache.james.mailbox.exception.OverQuotaException;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.metrics.api.TimeMetric;
+import org.apache.james.rrt.api.CanSendFrom;
import org.apache.james.server.core.Envelope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -86,7 +87,8 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
private final MessageAppender messageAppender;
private final MessageSender messageSender;
private final ReferenceUpdater referenceUpdater;
-
+ private final CanSendFrom canSendFrom;
+
@VisibleForTesting
@Inject
SetMessagesCreationProcessor(MessageFullViewFactory messageFullViewFactory,
@@ -97,7 +99,8 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
MailboxId.Factory mailboxIdFactory,
MessageAppender messageAppender,
MessageSender messageSender,
- ReferenceUpdater referenceUpdater) {
+ ReferenceUpdater referenceUpdater,
+ CanSendFrom canSendFrom) {
this.messageFullViewFactory = messageFullViewFactory;
this.systemMailboxesProvider = systemMailboxesProvider;
this.attachmentChecker = attachmentChecker;
@@ -107,6 +110,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
this.messageAppender = messageAppender;
this.messageSender = messageSender;
this.referenceUpdater = referenceUpdater;
+ this.canSendFrom = canSendFrom;
}
@Override
@@ -128,7 +132,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
assertIsUserOwnerOfMailboxes(mailboxIds, mailboxSession);
performCreate(create, responseBuilder, mailboxSession);
} catch (MailboxSendingNotAllowedException e) {
- responseBuilder.notCreated(create.getCreationId(),
+ responseBuilder.notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.from)
@@ -145,16 +149,16 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
.build());
} catch (AttachmentsNotFoundException e) {
- responseBuilder.notCreated(create.getCreationId(),
+ responseBuilder.notCreated(create.getCreationId(),
SetMessagesError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.attachments)
.attachmentsNotFound(e.getAttachmentIds())
.description("Attachment not found")
.build());
-
+
} catch (InvalidMailboxForCreationException e) {
- responseBuilder.notCreated(create.getCreationId(),
+ responseBuilder.notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.mailboxIds)
@@ -174,7 +178,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
buildSetErrorFromValidationResult(create.getValue().validate()));
} catch (MailboxNotFoundException e) {
- responseBuilder.notCreated(create.getCreationId(),
+ responseBuilder.notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.description(e.getMessage())
@@ -198,7 +202,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
} catch (MailboxException | MessagingException | IOException e) {
LOG.error("Unexpected error while creating message", e);
- responseBuilder.notCreated(create.getCreationId(),
+ responseBuilder.notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.description("unexpected error")
@@ -281,7 +285,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
}
private MessageWithId handleOutboxMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException, IOException {
- assertUserIsSender(session, entry.getValue().getFrom());
+ assertUserCanSendFrom(session.getUser(), entry.getValue().getFrom());
MetaDataWithContent newMessage = messageAppender.appendMessageInMailboxes(entry, toMailboxIds(entry), session);
MessageFullView jmapMessage = messageFullViewFactory.fromMetaDataWithContent(newMessage);
Envelope envelope = EnvelopeUtils.fromMessage(jmapMessage);
@@ -290,11 +294,12 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
return new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage);
}
- private void assertUserIsSender(MailboxSession session, Optional<DraftEmailer> from) throws MailboxSendingNotAllowedException {
+ @VisibleForTesting
+ void assertUserCanSendFrom(Username connectedUser, Optional<DraftEmailer> from) throws MailboxSendingNotAllowedException {
if (!from.flatMap(DraftEmailer::getEmail)
- .filter(email -> session.getUser().equals(Username.of(email)))
+ .filter(email -> canSendFrom.userCanSendFrom(connectedUser, Username.of(email)))
.isPresent()) {
- String allowedSender = session.getUser().asString();
+ String allowedSender = connectedUser.asString();
throw new MailboxSendingNotAllowedException(allowedSender);
}
}
@@ -304,7 +309,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
MessageFullView jmapMessage = messageFullViewFactory.fromMetaDataWithContent(newMessage);
return new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage);
}
-
+
private boolean isAppendToMailboxWithRole(Role role, CreationMessage entry, MailboxSession mailboxSession) throws MailboxException {
return getMailboxWithRole(mailboxSession, role)
.map(entry::isOnlyIn)
@@ -320,7 +325,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
private Optional<MessageManager> getMailboxWithRole(MailboxSession mailboxSession, Role role) throws MailboxException {
return systemMailboxesProvider.getMailboxByRole(role, mailboxSession.getUser()).findFirst();
}
-
+
private SetError buildSetErrorFromValidationResult(List<ValidationResult> validationErrors) {
return SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessorTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessorTest.java
index e554199..ade1101 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessorTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessorTest.java
@@ -20,6 +20,7 @@
package org.apache.james.jmap.draft.methods;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -28,11 +29,18 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.net.UnknownHostException;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.james.core.Domain;
import org.apache.james.core.Username;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.domainlist.api.DomainListException;
+import org.apache.james.domainlist.lib.DomainListConfiguration;
+import org.apache.james.domainlist.memory.MemoryDomainList;
import org.apache.james.jmap.draft.exceptions.MailboxNotOwnedException;
import org.apache.james.jmap.draft.model.CreationMessage;
import org.apache.james.jmap.draft.model.CreationMessage.DraftEmailer;
@@ -64,6 +72,12 @@ import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.TestMessageId;
import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.rrt.api.CanSendFrom;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.lib.Mapping;
+import org.apache.james.rrt.lib.MappingSource;
+import org.apache.james.rrt.lib.CanSendFromImpl;
+import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
import org.apache.james.util.OptionalUtils;
import org.apache.james.util.html.HtmlTextExtractor;
import org.apache.james.util.mime.MessageContentExtractor;
@@ -80,6 +94,7 @@ import com.google.common.collect.ImmutableMap;
public class SetMessagesCreationProcessorTest {
private static final Username USER = Username.of("user@example.com");
+ private static final Username OTHER_USER = Username.of("other@example.com");
private static final String OUTBOX = "outbox";
private static final InMemoryId OUTBOX_ID = InMemoryId.of(12345);
private static final String DRAFTS = "drafts";
@@ -108,6 +123,8 @@ public class SetMessagesCreationProcessorTest {
private AttachmentManager mockedAttachmentManager;
private MailboxManager mockedMailboxManager;
private Factory mockedMailboxIdFactory;
+ private MemoryRecipientRewriteTable recipientRewriteTable;
+ private CanSendFrom canSendFrom;
private SetMessagesCreationProcessor sut;
private MessageManager outbox;
private MessageManager drafts;
@@ -121,12 +138,23 @@ public class SetMessagesCreationProcessorTest {
private ReferenceUpdater referenceUpdater;
@Before
- public void setUp() throws MailboxException {
+ public void setUp() throws MailboxException, DomainListException, UnknownHostException, ConfigurationException {
MessageContentExtractor messageContentExtractor = new MessageContentExtractor();
HtmlTextExtractor htmlTextExtractor = new JsoupHtmlTextExtractor();
BlobManager blobManager = mock(BlobManager.class);
when(blobManager.toBlobId(any(MessageId.class))).thenReturn(org.apache.james.mailbox.model.BlobId.fromString("fake"));
MessageIdManager messageIdManager = mock(MessageIdManager.class);
+ recipientRewriteTable = new MemoryRecipientRewriteTable();
+
+ DNSService dnsService = mock(DNSService.class);
+ MemoryDomainList domainList = new MemoryDomainList(dnsService);
+ domainList.configure(DomainListConfiguration.builder()
+ .autoDetect(false)
+ .autoDetectIp(false));
+ domainList.addDomain(Domain.of("example.com"));
+ domainList.addDomain(Domain.of("other.org"));
+ recipientRewriteTable.setDomainList(domainList);
+ canSendFrom = new CanSendFromImpl(recipientRewriteTable);
messageFullViewFactory = new MessageFullViewFactory(blobManager, messageContentExtractor, htmlTextExtractor,
messageIdManager,
new MemoryMessageFastViewProjection(new RecordingMetricFactory()));
@@ -150,7 +178,8 @@ public class SetMessagesCreationProcessorTest {
mockedMailboxIdFactory,
messageAppender,
messageSender,
- referenceUpdater);
+ referenceUpdater,
+ canSendFrom);
outbox = mock(MessageManager.class);
when(mockedMailboxIdFactory.fromString(OUTBOX_ID.serialize()))
@@ -234,7 +263,7 @@ public class SetMessagesCreationProcessorTest {
@Test
public void processShouldReturnNonEmptyCreatedWhenRequestHasNonEmptyCreate() throws MailboxException {
// Given
- sut = new SetMessagesCreationProcessor(messageFullViewFactory, fakeSystemMailboxesProvider, new AttachmentChecker(mockedAttachmentManager), new RecordingMetricFactory(), mockedMailboxManager, mockedMailboxIdFactory, messageAppender, messageSender, referenceUpdater);
+ sut = new SetMessagesCreationProcessor(messageFullViewFactory, fakeSystemMailboxesProvider, new AttachmentChecker(mockedAttachmentManager), new RecordingMetricFactory(), mockedMailboxManager, mockedMailboxIdFactory, messageAppender, messageSender, referenceUpdater, canSendFrom);
// When
SetMessagesResponse result = sut.process(createMessageInOutbox, session);
@@ -253,7 +282,8 @@ public class SetMessagesCreationProcessorTest {
new AttachmentChecker(mockedAttachmentManager), new RecordingMetricFactory(), mockedMailboxManager, mockedMailboxIdFactory,
messageAppender,
messageSender,
- referenceUpdater);
+ referenceUpdater,
+ canSendFrom);
// When
SetMessagesResponse actual = sut.process(createMessageInOutbox, session);
@@ -370,7 +400,87 @@ public class SetMessagesCreationProcessorTest {
sut.assertIsUserOwnerOfMailboxes(ImmutableList.of(mailboxId), session);
}
-
+
+ @Test
+ public void assertUserCanSendFromShouldThrowWhenSenderIsNotTheConnectedUser() {
+ DraftEmailer sender = DraftEmailer
+ .builder()
+ .name("other")
+ .email("other@example.com")
+ .build();
+
+ assertThatThrownBy(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
+ .isInstanceOf(MailboxSendingNotAllowedException.class);
+ }
+
+ @Test
+ public void assertUserCanSendFromShouldNotThrowWhenSenderIsTheConnectedUser() {
+ DraftEmailer sender = DraftEmailer
+ .builder()
+ .name("user")
+ .email(USER.asString())
+ .build();
+
+ assertThatCode(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void assertUserCanSendFromShouldThrowWhenSenderIsAnAliasOfAnotherUser() throws Exception {
+ DraftEmailer sender = DraftEmailer
+ .builder()
+ .name("alias")
+ .email("alias@example.com")
+ .build();
+
+ recipientRewriteTable.addAliasMapping(MappingSource.fromUser("alias", "example.com"), OTHER_USER.asString());
+
+ assertThatThrownBy(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
+ .isInstanceOf(MailboxSendingNotAllowedException.class);
+ }
+
+ @Test
+ public void assertUserCanSendFromShouldNotThrowWhenSenderIsAnAliasOfTheConnectedUser() throws RecipientRewriteTableException {
+ DraftEmailer sender = DraftEmailer
+ .builder()
+ .name("alias")
+ .email("alias@example.com")
+ .build();
+
+ recipientRewriteTable.addAliasMapping(MappingSource.fromUser("alias", "example.com"), USER.asString());
+
+ assertThatCode(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void assertUserCanSendFromShouldNotThrowWhenSenderIsAnAliasOfTheConnectedUserFromADomainAlias() throws RecipientRewriteTableException {
+ DraftEmailer sender = DraftEmailer
+ .builder()
+ .name("user")
+ .email("user@other.org")
+ .build();
+
+ recipientRewriteTable.addMapping(MappingSource.fromDomain(Domain.of("other.org")), Mapping.domain(Domain.of("example.com")));
+
+ assertThatCode(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void assertUserCanSendFromShouldThrowWhenSenderIsAnAliasOfTheConnectedUserFromAGroupAlias() throws RecipientRewriteTableException {
+ DraftEmailer sender = DraftEmailer
+ .builder()
+ .name("group")
+ .email("group@example.com")
+ .build();
+
+ recipientRewriteTable.addGroupMapping(MappingSource.fromUser("group", "example.com"), USER.asString());
+
+ assertThatThrownBy(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
+ .isInstanceOf(MailboxSendingNotAllowedException.class);
+ }
+
public static class TestSystemMailboxesProvider implements SystemMailboxesProvider {
private final Supplier<Optional<MessageManager>> outboxSupplier;
diff --git a/server/testing/src/main/java/org/apache/james/jmap/JMAPTestingConstants.java b/server/testing/src/main/java/org/apache/james/jmap/JMAPTestingConstants.java
index b2df5d3..08c5f48 100644
--- a/server/testing/src/main/java/org/apache/james/jmap/JMAPTestingConstants.java
+++ b/server/testing/src/main/java/org/apache/james/jmap/JMAPTestingConstants.java
@@ -56,6 +56,7 @@ public interface JMAPTestingConstants {
String SECOND_ARGUMENTS = "[1][1]";
String DOMAIN = "domain.tld";
+ String DOMAIN_ALIAS = "domain-alias.tld";
Username BOB = Username.of("bob@" + DOMAIN);
String BOB_PASSWORD = "123456";
Username ALICE = Username.of("alice@" + DOMAIN);
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org