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