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 <rkowal...@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\": \"some...@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("u...@example.com"); + private static final Username OTHER_USER = Username.of("ot...@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("ot...@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("al...@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("al...@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("u...@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("gr...@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-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org