Merge pull request #559 from chibenwa/upgrade-homepage-dockerfile
[UPGRADE] Homepage dockerfile
diff --git a/.scalafix.conf b/.scalafix.conf
new file mode 100644
index 0000000..b00dda3
--- /dev/null
+++ b/.scalafix.conf
@@ -0,0 +1,26 @@
+rules = [
+ DisableSyntax,
+ ProcedureSyntax,
+ NoValInForComprehension,
+ LeakingImplicitClassVal
+]
+
+RemoveUnused.imports = true
+RemoveUnused.privates = false
+RemoveUnused.locals = true
+RemoveUnused.patternvars = false
+RemoveUnused.implicits = false
+
+DisableSyntax.noVars = false
+DisableSyntax.noThrows = false
+DisableSyntax.noNulls = false
+DisableSyntax.noReturns = false
+DisableSyntax.noWhileLoops = false
+DisableSyntax.noAsInstanceOf = false
+DisableSyntax.noIsInstanceOf = false
+DisableSyntax.noXml = false
+DisableSyntax.noDefaultArgs = false
+DisableSyntax.noFinalVal = true
+DisableSyntax.noFinalize = true
+DisableSyntax.noValPatterns = false
+DisableSyntax.noUniversalEquality = false
\ No newline at end of file
diff --git a/event-sourcing/event-sourcing-core/pom.xml b/event-sourcing/event-sourcing-core/pom.xml
index f843e5a..a7b6287 100644
--- a/event-sourcing/event-sourcing-core/pom.xml
+++ b/event-sourcing/event-sourcing-core/pom.xml
@@ -87,6 +87,13 @@
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <configuration>
+ <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/event-sourcing/event-sourcing-pojo/pom.xml b/event-sourcing/event-sourcing-pojo/pom.xml
index a6c5cc6..1d24afb 100644
--- a/event-sourcing/event-sourcing-pojo/pom.xml
+++ b/event-sourcing/event-sourcing-pojo/pom.xml
@@ -52,6 +52,13 @@
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <configuration>
+ <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
index 6ee804b..b117baf 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/MailboxManager.java
@@ -34,6 +34,7 @@
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.MessageRange;
import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
+import org.apache.james.mailbox.model.ThreadId;
import org.apache.james.mailbox.model.search.MailboxQuery;
import org.reactivestreams.Publisher;
@@ -322,7 +323,18 @@
* @param session
* the context for this call, not null
*/
- Publisher<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) throws MailboxException;
+ Publisher<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit);
+
+ /**
+ * Returns the list of MessageId of messages belonging to that Thread
+ *
+ * @param threadId
+ * target Thread
+ * @param session
+ * the context for this call, not null
+ * @return the list of MessageId of messages belonging to that Thread
+ */
+ Publisher<MessageId> getThread(ThreadId threadId, MailboxSession session);
/**
* Does the given mailbox exist?
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/exception/ThreadNotFoundException.java b/mailbox/api/src/main/java/org/apache/james/mailbox/exception/ThreadNotFoundException.java
new file mode 100644
index 0000000..f6f4c3e
--- /dev/null
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/exception/ThreadNotFoundException.java
@@ -0,0 +1,28 @@
+/******************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ******************************************************************/
+
+package org.apache.james.mailbox.exception;
+
+import org.apache.james.mailbox.model.ThreadId;
+
+public class ThreadNotFoundException extends MailboxException {
+ public ThreadNotFoundException(ThreadId threadId) {
+ super("Thread " + threadId.getBaseMessageId().serialize() + " can not be found");
+ }
+}
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java
index ec49426..919b6ad 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/ThreadId.java
@@ -21,10 +21,26 @@
import java.util.Objects;
+import javax.inject.Inject;
+
import com.google.common.base.MoreObjects;
public class ThreadId {
+ public static class Factory {
+ private final MessageId.Factory messageIdFactory;
+
+ @Inject
+ public Factory(MessageId.Factory messageIdFactory) {
+ this.messageIdFactory = messageIdFactory;
+ }
+
+ public ThreadId fromString(String serialized) {
+ MessageId messageId = messageIdFactory.fromString(serialized);
+ return fromBaseMessageId(messageId);
+ }
+ }
+
public static ThreadId fromBaseMessageId(MessageId baseMessageId) {
return new ThreadId(baseMessageId);
}
diff --git a/mailbox/event/json/pom.xml b/mailbox/event/json/pom.xml
index e232ab8..055f8ab 100644
--- a/mailbox/event/json/pom.xml
+++ b/mailbox/event/json/pom.xml
@@ -99,6 +99,13 @@
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <configuration>
+ <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+ </configuration>
+ </plugin>
</plugins>
</build>
diff --git a/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java b/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java
index 83ab8c1..dfa79f2 100644
--- a/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java
+++ b/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SearchThreadIdGuessingAlgorithmTest.java
@@ -19,21 +19,31 @@
package org.apache.james.mailbox.store.search;
+import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.inmemory.InMemoryCombinationManagerTestSystem;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.mailbox.inmemory.mail.InMemoryMapperProvider;
import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.store.CombinationManagerTestSystem;
import org.apache.james.mailbox.store.ThreadIdGuessingAlgorithmContract;
+import org.apache.james.mailbox.store.mail.MessageMapper;
import org.apache.james.mailbox.store.mail.SearchThreadIdGuessingAlgorithm;
import org.apache.james.mailbox.store.mail.ThreadIdGuessingAlgorithm;
+import org.apache.james.mailbox.store.mail.model.MapperProvider;
public class SearchThreadIdGuessingAlgorithmTest extends ThreadIdGuessingAlgorithmContract {
+ private InMemoryMailboxManager mailboxManager;
@Override
protected CombinationManagerTestSystem createTestingData() {
InMemoryIntegrationResources resources = InMemoryIntegrationResources.defaultResources();
+ mailboxManager = resources.getMailboxManager();
+ eventBus = resources.getEventBus();
+ messageIdFactory = resources.getMessageIdFactory();
+
return new InMemoryCombinationManagerTestSystem(
resources.getMailboxManager(),
resources.getMessageIdManager());
@@ -45,7 +55,22 @@
}
@Override
+ protected MessageMapper createMessageMapper(MailboxSession mailboxSession) {
+ return mailboxManager.getMapperFactory().createMessageMapper(mailboxSession);
+ }
+
+ @Override
+ protected MapperProvider provideMapper() {
+ return new InMemoryMapperProvider();
+ }
+
+ @Override
protected MessageId initNewBasedMessageId() {
return InMemoryMessageId.of(100);
}
+
+ @Override
+ protected MessageId initOtherBasedMessageId() {
+ return InMemoryMessageId.of(1000);
+ }
}
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index e228e7e..85d47e5 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -74,6 +74,7 @@
import org.apache.james.mailbox.model.MessageRange;
import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mailbox.model.ThreadId;
import org.apache.james.mailbox.model.UidValidity;
import org.apache.james.mailbox.model.search.MailboxQuery;
import org.apache.james.mailbox.model.search.PrefixedWildcard;
@@ -782,14 +783,19 @@
}
@Override
- public Flux<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) throws MailboxException {
+ public Flux<MessageId> search(MultimailboxesSearchQuery expression, MailboxSession session, long limit) {
return getInMailboxIds(expression, session)
.filter(id -> !expression.getNotInMailboxes().contains(id))
.collect(Guavate.toImmutableSet())
.flatMapMany(Throwing.function(ids -> index.search(session, ids, expression.getSearchQuery(), limit)));
}
- private Flux<MailboxId> getInMailboxIds(MultimailboxesSearchQuery expression, MailboxSession session) throws MailboxException {
+ @Override
+ public Flux<MessageId> getThread(ThreadId threadId, MailboxSession session) {
+ return threadIdGuessingAlgorithm.getMessageIdsInThread(threadId, session);
+ }
+
+ private Flux<MailboxId> getInMailboxIds(MultimailboxesSearchQuery expression, MailboxSession session) {
if (expression.getInMailboxes().isEmpty()) {
return accessibleMailboxIds(expression.getNamespace(), Right.Read, session);
} else {
@@ -799,7 +805,7 @@
}
}
- private Flux<Mailbox> filterReadable(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) throws MailboxException {
+ private Flux<Mailbox> filterReadable(ImmutableSet<MailboxId> inMailboxes, MailboxSession session) {
MailboxMapper mailboxMapper = mailboxSessionMapperFactory.getMailboxMapper(session);
return Flux.fromIterable(inMailboxes)
.concatMap(mailboxMapper::findMailboxById)
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java
index b6334ca..eed7eb2 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/NaiveThreadIdGuessingAlgorithm.java
@@ -28,6 +28,7 @@
import org.apache.james.mailbox.store.mail.model.MimeMessageId;
import org.apache.james.mailbox.store.mail.model.Subject;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class NaiveThreadIdGuessingAlgorithm implements ThreadIdGuessingAlgorithm {
@@ -35,4 +36,9 @@
public Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> thisMimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) {
return Mono.just(ThreadId.fromBaseMessageId(messageId));
}
+
+ @Override
+ public Flux<MessageId> getMessageIdsInThread(ThreadId threadId, MailboxSession session) {
+ return Flux.just(threadId.getBaseMessageId());
+ }
}
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java
index a229fee..551e737 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/SearchThreadIdGuessingAlgorithm.java
@@ -29,7 +29,7 @@
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageIdManager;
-import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.exception.ThreadNotFoundException;
import org.apache.james.mailbox.model.FetchGroup;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.MessageResult;
@@ -56,7 +56,7 @@
}
@Override
- public Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) throws MailboxException {
+ public Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) {
MultimailboxesSearchQuery expression = buildSearchQuery(mimeMessageId, inReplyTo, references, subject);
return Flux.from(mailboxManager.search(expression, session, 1))
@@ -67,6 +67,21 @@
.switchIfEmpty(Mono.just(ThreadId.fromBaseMessageId(messageId)));
}
+ @Override
+ public Flux<MessageId> getMessageIdsInThread(ThreadId threadId, MailboxSession session) {
+ SearchQuery searchQuery = SearchQuery.builder()
+ .andCriteria(SearchQuery.threadId(threadId))
+ .sorts(new SearchQuery.Sort(SearchQuery.Sort.SortClause.Arrival, SearchQuery.Sort.Order.NATURAL))
+ .build();
+
+ MultimailboxesSearchQuery expression = MultimailboxesSearchQuery
+ .from(searchQuery)
+ .build();
+
+ return Flux.from(mailboxManager.search(expression, session, Integer.MAX_VALUE))
+ .switchIfEmpty(Mono.error(() -> new ThreadNotFoundException(threadId)));
+ }
+
private MultimailboxesSearchQuery buildSearchQuery(Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject) {
Set<MimeMessageId> mimeMessageIds = buildMimeMessageIdSet(mimeMessageId, inReplyTo, references);
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java
index 4461fc9..2b6d91a 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/ThreadIdGuessingAlgorithm.java
@@ -23,14 +23,16 @@
import java.util.Optional;
import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.ThreadId;
import org.apache.james.mailbox.store.mail.model.MimeMessageId;
import org.apache.james.mailbox.store.mail.model.Subject;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ThreadIdGuessingAlgorithm {
- Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> thisMimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session) throws MailboxException;
+ Mono<ThreadId> guessThreadIdReactive(MessageId messageId, Optional<MimeMessageId> thisMimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject, MailboxSession session);
+
+ Flux<MessageId> getMessageIdsInThread(ThreadId threadId, MailboxSession session);
}
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java
index c28757c..c54ee33 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/ThreadIdGuessingAlgorithmContract.java
@@ -20,22 +20,40 @@
package org.apache.james.mailbox.store;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.nio.charset.StandardCharsets;
+import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
+import javax.mail.Flags;
+
import org.apache.james.core.Username;
+import org.apache.james.events.EventBus;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.events.MailboxIdRegistrationKey;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.exception.ThreadNotFoundException;
+import org.apache.james.mailbox.model.ByteContent;
+import org.apache.james.mailbox.model.Mailbox;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageMetaData;
import org.apache.james.mailbox.model.ThreadId;
+import org.apache.james.mailbox.model.UidValidity;
+import org.apache.james.mailbox.store.event.EventFactory;
+import org.apache.james.mailbox.store.mail.MessageMapper;
import org.apache.james.mailbox.store.mail.ThreadIdGuessingAlgorithm;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.MapperProvider;
import org.apache.james.mailbox.store.mail.model.MimeMessageId;
import org.apache.james.mailbox.store.mail.model.Subject;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.mime4j.stream.RawField;
import org.junit.jupiter.api.BeforeEach;
@@ -44,35 +62,55 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import com.google.common.collect.ImmutableList;
+
+import reactor.core.publisher.Flux;
+
public abstract class ThreadIdGuessingAlgorithmContract {
public static final Username USER = Username.of("quan");
+ private static final UidValidity UID_VALIDITY = UidValidity.of(42);
+
+ protected EventBus eventBus;
+ protected MessageId.Factory messageIdFactory;
private MailboxManager mailboxManager;
private MessageManager inbox;
+ private MessageMapper messageMapper;
private ThreadIdGuessingAlgorithm testee;
private MailboxSession mailboxSession;
private CombinationManagerTestSystem testingData;
private MessageId newBasedMessageId;
+ private MessageId otherBasedMessageId;
+ private Mailbox mailbox;
protected abstract CombinationManagerTestSystem createTestingData();
protected abstract ThreadIdGuessingAlgorithm initThreadIdGuessingAlgorithm(CombinationManagerTestSystem testingData);
+ protected abstract MessageMapper createMessageMapper(MailboxSession mailboxSession);
+
+ protected abstract MapperProvider provideMapper();
+
protected abstract MessageId initNewBasedMessageId();
+ protected abstract MessageId initOtherBasedMessageId();
+
@BeforeEach
void setUp() throws Exception {
testingData = createTestingData();
testee = initThreadIdGuessingAlgorithm(testingData);
newBasedMessageId = initNewBasedMessageId();
+ otherBasedMessageId = initOtherBasedMessageId();
mailboxManager = testingData.getMailboxManager();
mailboxSession = mailboxManager.createSystemSession(USER);
mailboxManager.createMailbox(MailboxPath.inbox(USER), mailboxSession);
+ messageMapper = createMessageMapper(mailboxSession);
inbox = mailboxManager.getMailbox(MailboxPath.inbox(USER), mailboxSession);
+ mailbox = inbox.getMailboxEntity();
}
@Test
- void givenNonMailWhenAddAMailThenGuessingThreadIdShouldBasedOnGeneratedMessageId() throws Exception {
+ void givenNonMailWhenAddAMailThenGuessingThreadIdShouldBasedOnGeneratedMessageId() {
ThreadId threadId = testee.guessThreadIdReactive(newBasedMessageId, Optional.of(new MimeMessageId("abc")), Optional.empty(), Optional.empty(), Optional.of(new Subject("test")), mailboxSession).block();
assertThat(threadId.getBaseMessageId()).isEqualTo(newBasedMessageId);
@@ -202,4 +240,86 @@
assertThat(threadId.getBaseMessageId()).isEqualTo(newBasedMessageId);
}
+ @Test
+ void givenThreeMailsInAThreadThenGetThreadShouldReturnAListWithThreeMessageIdsSortedByArrivalDate() throws MailboxException {
+ MailboxMessage message1 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+ MailboxMessage message2 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+ MailboxMessage message3 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+
+ appendMessageThenDispatchAddedEvent(mailbox, message1);
+ appendMessageThenDispatchAddedEvent(mailbox, message2);
+ appendMessageThenDispatchAddedEvent(mailbox, message3);
+
+ Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
+
+ assertThat(messageIds.collectList().block())
+ .isEqualTo(ImmutableList.of(message1.getMessageId(), message2.getMessageId(), message3.getMessageId()));
+ }
+
+ @Test
+ void givenNonMailInAThreadThenGetThreadShouldThrowThreadNotFoundException() {
+ Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
+
+ assertThatThrownBy(() -> testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession).collectList().block())
+ .getCause()
+ .isInstanceOf(ThreadNotFoundException.class);
+ }
+
+ @Test
+ void givenAMailInAThreadThenGetThreadShouldReturnAListWithOnlyOneMessageIdInThatThread() throws MailboxException {
+ MailboxMessage message1 = createMessage(mailbox, ThreadId.fromBaseMessageId(newBasedMessageId));
+
+ appendMessageThenDispatchAddedEvent(mailbox, message1);
+
+ Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(newBasedMessageId), mailboxSession);
+
+ assertThat(messageIds.collectList().block())
+ .containsOnly(message1.getMessageId());
+ }
+
+ @Test
+ void givenTwoDistinctThreadsThenGetThreadShouldNotReturnUnrelatedMails() throws MailboxException {
+ // given message1 and message2 in thread1, message3 in thread2
+ ThreadId threadId1 = ThreadId.fromBaseMessageId(newBasedMessageId);
+ ThreadId threadId2 = ThreadId.fromBaseMessageId(otherBasedMessageId);
+ MailboxMessage message1 = createMessage(mailbox, threadId1);
+ MailboxMessage message2 = createMessage(mailbox, threadId1);
+ MailboxMessage message3 = createMessage(mailbox, threadId2);
+
+ appendMessageThenDispatchAddedEvent(mailbox, message1);
+ appendMessageThenDispatchAddedEvent(mailbox, message2);
+ appendMessageThenDispatchAddedEvent(mailbox, message3);
+
+ // then get thread2 should not return unrelated message1 and message2
+ Flux<MessageId> messageIds = testee.getMessageIdsInThread(ThreadId.fromBaseMessageId(otherBasedMessageId), mailboxSession);
+
+ assertThat(messageIds.collectList().block())
+ .doesNotContain(message1.getMessageId(), message2.getMessageId());
+ }
+
+ private SimpleMailboxMessage createMessage(Mailbox mailbox, ThreadId threadId) {
+ MessageId messageId = messageIdFactory.generate();
+ String content = "Some content";
+ int bodyStart = 16;
+ return new SimpleMailboxMessage(messageId,
+ threadId,
+ new Date(),
+ content.length(),
+ bodyStart,
+ new ByteContent(content.getBytes()),
+ new Flags(),
+ new PropertyBuilder().build(),
+ mailbox.getMailboxId());
+ }
+
+ private void appendMessageThenDispatchAddedEvent(Mailbox mailbox, MailboxMessage mailboxMessage) throws MailboxException {
+ MessageMetaData messageMetaData = messageMapper.add(mailbox, mailboxMessage);
+ eventBus.dispatch(EventFactory.added()
+ .randomEventId()
+ .mailboxSession(mailboxSession)
+ .mailbox(mailbox)
+ .addMetaData(messageMetaData)
+ .build(),
+ new MailboxIdRegistrationKey(mailbox.getMailboxId())).block();
+ }
}
diff --git a/mdn/pom.xml b/mdn/pom.xml
index 735a372..24b9687 100644
--- a/mdn/pom.xml
+++ b/mdn/pom.xml
@@ -90,6 +90,13 @@
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <configuration>
+ <config>${project.parent.basedir}/.scalafix.conf</config>
+ </configuration>
+ </plugin>
</plugins>
</build>
diff --git a/pom.xml b/pom.xml
index 2009d47..5ed7565 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1358,6 +1358,12 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-data-ldap</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-data-library</artifactId>
<version>${project.version}</version>
</dependency>
@@ -2819,6 +2825,24 @@
<version>2.7.1</version>
</plugin>
<plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <version>0.1.4_0.9.23</version>
+ <configuration>
+ <config>${basedir}/.scalafix.conf</config>
+ <mode>CHECK</mode>
+ </configuration>
+ <executions>
+ <execution>
+ <id>scala-check-style</id>
+ <goals>
+ <goal>scalafix</goal>
+ </goals>
+ <phase>compile</phase>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>2.0.0</version>
@@ -2847,7 +2871,15 @@
<arg>-unchecked</arg>
<arg>-deprecation</arg>
<arg>-explaintypes</arg>
+ <arg>-Ywarn-unused</arg>
</args>
+ <compilerPlugins>
+ <compilerPlugin>
+ <groupId>org.scalameta</groupId>
+ <artifactId>semanticdb-scalac_${scala.version}</artifactId>
+ <version>4.4.23</version>
+ </compilerPlugin>
+ </compilerPlugins>
</configuration>
<executions>
<execution>
diff --git a/server/apps/cassandra-app/pom.xml b/server/apps/cassandra-app/pom.xml
index 51d93b5..11dd2a8 100644
--- a/server/apps/cassandra-app/pom.xml
+++ b/server/apps/cassandra-app/pom.xml
@@ -106,7 +106,6 @@
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-data-ldap</artifactId>
- <version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
@@ -126,6 +125,12 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-data-ldap</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-elasticsearch</artifactId>
</dependency>
<dependency>
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java
index 0c3a91a..b248f47 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java
+++ b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJamesServerTest.java
@@ -29,6 +29,7 @@
import org.apache.commons.net.imap.IMAPClient;
import org.apache.james.core.Domain;
+import org.apache.james.data.LdapTestExtension;
import org.apache.james.data.UsersRepositoryModuleChooser;
import org.apache.james.modules.TestJMAPServerModule;
import org.apache.james.modules.protocols.ImapGuiceProbe;
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java
index 536fabc..6bcb98e 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java
+++ b/server/apps/cassandra-app/src/test/java/org/apache/james/CassandraLdapJmapJamesServerTest.java
@@ -19,6 +19,7 @@
package org.apache.james;
+import org.apache.james.data.LdapTestExtension;
import org.apache.james.data.UsersRepositoryModuleChooser;
import org.apache.james.jmap.draft.JmapJamesServerContract;
import org.apache.james.modules.TestJMAPServerModule;
diff --git a/server/apps/distributed-app/pom.xml b/server/apps/distributed-app/pom.xml
index acfaaf7..9e90b98 100644
--- a/server/apps/distributed-app/pom.xml
+++ b/server/apps/distributed-app/pom.xml
@@ -141,7 +141,6 @@
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-data-ldap</artifactId>
- <version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
@@ -161,6 +160,12 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-data-ldap</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-distributed</artifactId>
</dependency>
<dependency>
diff --git a/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java b/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java
index 623d15e..40b6f4b 100644
--- a/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java
+++ b/server/apps/distributed-app/src/test/java/org/apache/james/CassandraRabbitMQLdapJmapJamesServerTest.java
@@ -25,6 +25,7 @@
import java.io.IOException;
import org.apache.commons.net.imap.IMAPClient;
+import org.apache.james.data.LdapTestExtension;
import org.apache.james.data.UsersRepositoryModuleChooser;
import org.apache.james.jmap.draft.JmapJamesServerContract;
import org.apache.james.modules.AwsS3BlobStoreExtension;
diff --git a/server/apps/jpa-app/pom.xml b/server/apps/jpa-app/pom.xml
index cc395da..7727845 100644
--- a/server/apps/jpa-app/pom.xml
+++ b/server/apps/jpa-app/pom.xml
@@ -72,6 +72,12 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-data-ldap</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-common</artifactId>
</dependency>
<dependency>
@@ -82,6 +88,16 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-data-ldap</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-data-ldap</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-es-resporter</artifactId>
</dependency>
<dependency>
diff --git a/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesConfiguration.java b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
new file mode 100644
index 0000000..de35304
--- /dev/null
+++ b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
@@ -0,0 +1,127 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.james.data.UsersRepositoryModuleChooser;
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.filesystem.api.JamesDirectoriesProvider;
+import org.apache.james.server.core.JamesServerResourceLoader;
+import org.apache.james.server.core.MissingArgumentException;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+
+public class JPAJamesConfiguration implements Configuration {
+ public static class Builder {
+ private Optional<String> rootDirectory;
+ private Optional<ConfigurationPath> configurationPath;
+ private Optional<UsersRepositoryModuleChooser.Implementation> usersRepositoryImplementation;
+
+ private Builder() {
+ rootDirectory = Optional.empty();
+ configurationPath = Optional.empty();
+ usersRepositoryImplementation = Optional.empty();
+ }
+
+ public Builder workingDirectory(String path) {
+ rootDirectory = Optional.of(path);
+ return this;
+ }
+
+ public Builder workingDirectory(File file) {
+ rootDirectory = Optional.of(file.getAbsolutePath());
+ return this;
+ }
+
+ public Builder useWorkingDirectoryEnvProperty() {
+ rootDirectory = Optional.ofNullable(System.getProperty(WORKING_DIRECTORY));
+ if (!rootDirectory.isPresent()) {
+ throw new MissingArgumentException("Server needs a working.directory env entry");
+ }
+ return this;
+ }
+
+ public Builder configurationPath(ConfigurationPath path) {
+ configurationPath = Optional.of(path);
+ return this;
+ }
+
+ public Builder configurationFromClasspath() {
+ configurationPath = Optional.of(new ConfigurationPath(FileSystem.CLASSPATH_PROTOCOL));
+ return this;
+ }
+
+ public Builder usersRepository(UsersRepositoryModuleChooser.Implementation implementation) {
+ this.usersRepositoryImplementation = Optional.of(implementation);
+ return this;
+ }
+
+ public JPAJamesConfiguration build() {
+ ConfigurationPath configurationPath = this.configurationPath.orElse(new ConfigurationPath(FileSystem.FILE_PROTOCOL_AND_CONF));
+ JamesServerResourceLoader directories = new JamesServerResourceLoader(rootDirectory
+ .orElseThrow(() -> new MissingArgumentException("Server needs a working.directory env entry")));
+
+ FileSystemImpl fileSystem = new FileSystemImpl(directories);
+
+ FileConfigurationProvider configurationProvider = new FileConfigurationProvider(fileSystem, Basic.builder()
+ .configurationPath(configurationPath)
+ .workingDirectory(directories.getRootDirectory())
+ .build());
+ UsersRepositoryModuleChooser.Implementation usersRepositoryChoice = usersRepositoryImplementation.orElseGet(
+ () -> UsersRepositoryModuleChooser.Implementation.parse(configurationProvider));
+
+ return new JPAJamesConfiguration(
+ configurationPath,
+ directories,
+ usersRepositoryChoice);
+ }
+ }
+
+ public static JPAJamesConfiguration.Builder builder() {
+ return new Builder();
+ }
+
+ private final ConfigurationPath configurationPath;
+ private final JamesDirectoriesProvider directories;
+ private final UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation;
+
+ public JPAJamesConfiguration(ConfigurationPath configurationPath, JamesDirectoriesProvider directories, UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation) {
+ this.configurationPath = configurationPath;
+ this.directories = directories;
+ this.usersRepositoryImplementation = usersRepositoryImplementation;
+ }
+
+ @Override
+ public ConfigurationPath configurationPath() {
+ return configurationPath;
+ }
+
+ @Override
+ public JamesDirectoriesProvider directories() {
+ return directories;
+ }
+
+ public UsersRepositoryModuleChooser.Implementation getUsersRepositoryImplementation() {
+ return usersRepositoryImplementation;
+ }
+}
diff --git a/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java
index 7b33cb6..a840ee6 100644
--- a/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java
+++ b/server/apps/jpa-app/src/main/java/org/apache/james/JPAJamesServerMain.java
@@ -19,9 +19,11 @@
package org.apache.james;
+import org.apache.james.data.UsersRepositoryModuleChooser;
import org.apache.james.modules.MailboxModule;
import org.apache.james.modules.MailetProcessingModule;
import org.apache.james.modules.data.JPADataModule;
+import org.apache.james.modules.data.JPAUsersRepositoryModule;
import org.apache.james.modules.data.SieveJPARepositoryModules;
import org.apache.james.modules.mailbox.DefaultEventModule;
import org.apache.james.modules.mailbox.JPAMailboxModule;
@@ -51,7 +53,6 @@
import org.apache.james.modules.server.WebAdminReIndexingTaskSerializationModule;
import org.apache.james.modules.server.WebAdminServerModule;
import org.apache.james.modules.spamassassin.SpamAssassinListenerModule;
-import org.apache.james.server.core.configuration.Configuration;
import com.google.inject.Module;
import com.google.inject.util.Modules;
@@ -99,7 +100,7 @@
new MailetProcessingModule(), JPA_SERVER_MODULE, PROTOCOLS);
public static void main(String[] args) throws Exception {
- Configuration configuration = Configuration.builder()
+ JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
.useWorkingDirectoryEnvProperty()
.build();
@@ -110,9 +111,10 @@
JamesServerMain.main(server);
}
- static GuiceJamesServer createServer(Configuration configuration) {
+ static GuiceJamesServer createServer(JPAJamesConfiguration configuration) {
return GuiceJamesServer.forConfiguration(configuration)
- .combineWith(JPA_MODULE_AGGREGATE);
+ .combineWith(JPA_MODULE_AGGREGATE)
+ .combineWith(new UsersRepositoryModuleChooser(new JPAUsersRepositoryModule())
+ .chooseModules(configuration.getUsersRepositoryImplementation()));
}
-
}
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java
index 9cd0cd6..db1284b 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerTest.java
@@ -19,6 +19,7 @@
package org.apache.james;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Durations.FIVE_HUNDRED_MILLISECONDS;
import static org.awaitility.Durations.ONE_MINUTE;
@@ -41,7 +42,12 @@
class JPAJamesServerTest implements JamesServerContract {
@RegisterExtension
- static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+ JPAJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .usersRepository(DEFAULT)
+ .build())
.server(configuration -> JPAJamesServerMain.createServer(configuration)
.overrideWith(new TestJPAConfigurationModule()))
.lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java
index d8359e4..95f14b4 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest.java
@@ -19,12 +19,19 @@
package org.apache.james;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+
import org.junit.jupiter.api.extension.RegisterExtension;
class JPAJamesServerWithAuthenticatedDatabaseSqlValidationTest extends JPAJamesServerWithSqlValidationTest {
@RegisterExtension
- static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+ JPAJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .usersRepository(DEFAULT)
+ .build())
.server(configuration -> JPAJamesServerMain.createServer(configuration)
.overrideWith(new TestJPAConfigurationModuleWithSqlValidation.WithDatabaseAuthentication()))
.lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java
index 89f4dc0..2581b10 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest.java
@@ -19,12 +19,18 @@
package org.apache.james;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
+
import org.junit.jupiter.api.extension.RegisterExtension;
class JPAJamesServerWithNoDatabaseAuthenticaticationSqlValidationTest extends JPAJamesServerWithSqlValidationTest {
-
@RegisterExtension
- static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+ JPAJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .usersRepository(DEFAULT)
+ .build())
.server(configuration -> JPAJamesServerMain.createServer(configuration)
.overrideWith(new TestJPAConfigurationModuleWithSqlValidation.NoDatabaseAuthentication()))
.lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JPAWithLDAPJamesServerTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JPAWithLDAPJamesServerTest.java
new file mode 100644
index 0000000..8d86124
--- /dev/null
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JPAWithLDAPJamesServerTest.java
@@ -0,0 +1,57 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james;
+
+import static org.apache.james.MailsShouldBeWellReceived.JAMES_SERVER_HOST;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.LDAP;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+
+import org.apache.commons.net.imap.IMAPClient;
+import org.apache.james.data.LdapTestExtension;
+import org.apache.james.modules.protocols.ImapGuiceProbe;
+import org.apache.james.user.ldap.DockerLdapSingleton;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class JPAWithLDAPJamesServerTest {
+ @RegisterExtension
+ static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+ JPAJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .usersRepository(LDAP)
+ .build())
+ .server(configuration -> JPAJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJPAConfigurationModule()))
+ .lifeCycle(JamesServerExtension.Lifecycle.PER_CLASS)
+ .extension(new LdapTestExtension())
+ .build();
+
+
+ @Test
+ void userFromLdapShouldLoginViaImapProtocol(GuiceJamesServer server) throws IOException {
+ IMAPClient imapClient = new IMAPClient();
+ imapClient.connect(JAMES_SERVER_HOST, server.getProbe(ImapGuiceProbe.class).getImapPort());
+
+ assertThat(imapClient.login(DockerLdapSingleton.JAMES_USER.asString(), DockerLdapSingleton.PASSWORD)).isTrue();
+ }
+}
diff --git a/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java b/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java
index 3150e58..c08f88c 100644
--- a/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java
+++ b/server/apps/jpa-app/src/test/java/org/apache/james/JamesCapabilitiesServerTest.java
@@ -18,6 +18,7 @@
****************************************************************/
package org.apache.james;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -40,7 +41,12 @@
}
@RegisterExtension
- static JamesServerExtension jamesServerExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ static JamesServerExtension jamesServerExtension = new JamesServerBuilder<JPAJamesConfiguration>(tmpDir ->
+ JPAJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .usersRepository(DEFAULT)
+ .build())
.server(configuration -> JPAJamesServerMain.createServer(configuration)
.overrideWith(new TestJPAConfigurationModule())
.overrideWith(binder -> binder.bind(MailboxManager.class).toInstance(mailboxManager())))
diff --git a/server/apps/jpa-smtp-app/pom.xml b/server/apps/jpa-smtp-app/pom.xml
index ced7ff6..84c8cee 100644
--- a/server/apps/jpa-smtp-app/pom.xml
+++ b/server/apps/jpa-smtp-app/pom.xml
@@ -69,6 +69,13 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-data-ldap</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-common</artifactId>
</dependency>
<dependency>
@@ -79,6 +86,10 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-data-ldap</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-es-resporter</artifactId>
</dependency>
<dependency>
diff --git a/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesConfiguration.java b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
new file mode 100644
index 0000000..9614230
--- /dev/null
+++ b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesConfiguration.java
@@ -0,0 +1,127 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.james.data.UsersRepositoryModuleChooser;
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.filesystem.api.JamesDirectoriesProvider;
+import org.apache.james.server.core.JamesServerResourceLoader;
+import org.apache.james.server.core.MissingArgumentException;
+import org.apache.james.server.core.configuration.Configuration;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+
+public class JPAJamesConfiguration implements Configuration {
+ public static class Builder {
+ private Optional<String> rootDirectory;
+ private Optional<ConfigurationPath> configurationPath;
+ private Optional<UsersRepositoryModuleChooser.Implementation> usersRepositoryImplementation;
+
+ private Builder() {
+ rootDirectory = Optional.empty();
+ configurationPath = Optional.empty();
+ usersRepositoryImplementation = Optional.empty();
+ }
+
+ public Builder workingDirectory(String path) {
+ rootDirectory = Optional.of(path);
+ return this;
+ }
+
+ public Builder workingDirectory(File file) {
+ rootDirectory = Optional.of(file.getAbsolutePath());
+ return this;
+ }
+
+ public Builder useWorkingDirectoryEnvProperty() {
+ rootDirectory = Optional.ofNullable(System.getProperty(WORKING_DIRECTORY));
+ if (!rootDirectory.isPresent()) {
+ throw new MissingArgumentException("Server needs a working.directory env entry");
+ }
+ return this;
+ }
+
+ public Builder configurationPath(ConfigurationPath path) {
+ configurationPath = Optional.of(path);
+ return this;
+ }
+
+ public Builder configurationFromClasspath() {
+ configurationPath = Optional.of(new ConfigurationPath(FileSystem.CLASSPATH_PROTOCOL));
+ return this;
+ }
+
+ public Builder usersRepository(UsersRepositoryModuleChooser.Implementation implementation) {
+ this.usersRepositoryImplementation = Optional.of(implementation);
+ return this;
+ }
+
+ public JPAJamesConfiguration build() {
+ ConfigurationPath configurationPath = this.configurationPath.orElse(new ConfigurationPath(FileSystem.FILE_PROTOCOL_AND_CONF));
+ JamesServerResourceLoader directories = new JamesServerResourceLoader(rootDirectory
+ .orElseThrow(() -> new MissingArgumentException("Server needs a working.directory env entry")));
+
+ FileSystemImpl fileSystem = new FileSystemImpl(directories);
+
+ FileConfigurationProvider configurationProvider = new FileConfigurationProvider(fileSystem, Basic.builder()
+ .configurationPath(configurationPath)
+ .workingDirectory(directories.getRootDirectory())
+ .build());
+ UsersRepositoryModuleChooser.Implementation usersRepositoryChoice = usersRepositoryImplementation.orElseGet(
+ () -> UsersRepositoryModuleChooser.Implementation.parse(configurationProvider));
+
+ return new JPAJamesConfiguration(
+ configurationPath,
+ directories,
+ usersRepositoryChoice);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final ConfigurationPath configurationPath;
+ private final JamesDirectoriesProvider directories;
+ private final UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation;
+
+ public JPAJamesConfiguration(ConfigurationPath configurationPath, JamesDirectoriesProvider directories, UsersRepositoryModuleChooser.Implementation usersRepositoryImplementation) {
+ this.configurationPath = configurationPath;
+ this.directories = directories;
+ this.usersRepositoryImplementation = usersRepositoryImplementation;
+ }
+
+ @Override
+ public ConfigurationPath configurationPath() {
+ return configurationPath;
+ }
+
+ @Override
+ public JamesDirectoriesProvider directories() {
+ return directories;
+ }
+
+ public UsersRepositoryModuleChooser.Implementation getUsersRepositoryImplementation() {
+ return usersRepositoryImplementation;
+ }
+}
diff --git a/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java
index bc6d1fd..a7f684c 100644
--- a/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java
+++ b/server/apps/jpa-smtp-app/src/main/java/org/apache/james/JPAJamesServerMain.java
@@ -19,9 +19,11 @@
package org.apache.james;
+import org.apache.james.data.UsersRepositoryModuleChooser;
import org.apache.james.modules.MailetProcessingModule;
import org.apache.james.modules.data.JPADataModule;
import org.apache.james.modules.data.JPAEntityManagerModule;
+import org.apache.james.modules.data.JPAUsersRepositoryModule;
import org.apache.james.modules.protocols.ProtocolHandlerModule;
import org.apache.james.modules.protocols.SMTPServerModule;
import org.apache.james.modules.queue.activemq.ActiveMQQueueModule;
@@ -35,7 +37,6 @@
import org.apache.james.modules.server.RawPostDequeueDecoratorModule;
import org.apache.james.modules.server.TaskManagerModule;
import org.apache.james.modules.server.WebAdminServerModule;
-import org.apache.james.server.core.configuration.Configuration;
import com.google.inject.Module;
import com.google.inject.util.Modules;
@@ -62,7 +63,7 @@
new ElasticSearchMetricReporterModule());
public static void main(String[] args) throws Exception {
- Configuration configuration = Configuration.builder()
+ JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
.useWorkingDirectoryEnvProperty()
.build();
@@ -72,9 +73,11 @@
JamesServerMain.main(server);
}
- public static GuiceJamesServer createServer(Configuration configuration) {
+ public static GuiceJamesServer createServer(JPAJamesConfiguration configuration) {
return GuiceJamesServer.forConfiguration(configuration)
- .combineWith(JPA_SERVER_MODULE, PROTOCOLS, new DKIMMailetModule());
+ .combineWith(JPA_SERVER_MODULE, PROTOCOLS, new DKIMMailetModule())
+ .combineWith(new UsersRepositoryModuleChooser(new JPAUsersRepositoryModule())
+ .chooseModules(configuration.getUsersRepositoryImplementation()));
}
}
diff --git a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
index 542022a..cdbeaae 100644
--- a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
+++ b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/JPAJamesServerTest.java
@@ -19,6 +19,7 @@
package org.apache.james;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
@@ -34,7 +35,6 @@
import org.apache.james.mailrepository.jpa.JPAUrl;
import org.apache.james.modules.protocols.SmtpGuiceProbe;
import org.apache.james.rrt.jpa.model.JPARecipientRewrite;
-import org.apache.james.server.core.configuration.Configuration;
import org.apache.james.user.jpa.model.JPAUser;
import org.junit.After;
import org.junit.Before;
@@ -56,9 +56,10 @@
}
private org.apache.james.GuiceJamesServer createJamesServer() throws IOException {
- Configuration configuration = Configuration.builder()
+ JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
.workingDirectory(temporaryFolder.newFolder())
.configurationFromClasspath()
+ .usersRepository(DEFAULT)
.build();
return JPAJamesServerMain.createServer(configuration)
diff --git a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java
index be9a34b..50df95b 100644
--- a/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java
+++ b/server/apps/jpa-smtp-app/src/test/java/org/apache/james/mariadb/JPAMariaDBJamesServerTest.java
@@ -19,6 +19,7 @@
package org.apache.james.mariadb;
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
@@ -28,9 +29,9 @@
import java.nio.charset.StandardCharsets;
import org.apache.james.GuiceJamesServer;
+import org.apache.james.JPAJamesConfiguration;
import org.apache.james.JPAJamesServerMain;
import org.apache.james.modules.protocols.SmtpGuiceProbe;
-import org.apache.james.server.core.configuration.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -62,8 +63,9 @@
}
private org.apache.james.GuiceJamesServer createJamesServer(String mariaDBUrl) throws IOException {
- Configuration configuration = Configuration.builder()
+ JPAJamesConfiguration configuration = JPAJamesConfiguration.builder()
.workingDirectory(temporaryFolder.newFolder())
+ .usersRepository(DEFAULT)
.configurationFromClasspath()
.build();
diff --git a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
index 8f45e0f..aa4a6b6 100644
--- a/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
+++ b/server/blob/blob-aes/src/main/java/org/apache/james/blob/aes/AESBlobStoreDAO.java
@@ -131,4 +131,9 @@
public Publisher<Void> deleteBucket(BucketName bucketName) {
return underlying.deleteBucket(bucketName);
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return underlying.listBuckets();
+ }
}
diff --git a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
index 8be3728..ad58cd0 100644
--- a/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
+++ b/server/blob/blob-aes/src/test/java/org/apache/james/blob/aes/AESBlobStoreDAOTest.java
@@ -30,6 +30,7 @@
import org.apache.james.blob.api.BlobStoreDAOContract;
import org.apache.james.blob.memory.MemoryBlobStoreDAO;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import com.google.common.io.ByteSource;
@@ -83,4 +84,10 @@
assertThat(bytes).isNotEqualTo(SHORT_BYTEARRAY);
}
+
+ @Override
+ @Disabled("Not supported by the Memory blob store")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+ }
}
\ No newline at end of file
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
index 3f7cd67..cf315d2 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStore.java
@@ -58,6 +58,8 @@
BucketName getDefaultBucketName();
+ Publisher<BucketName> listBuckets();
+
Publisher<Void> deleteBucket(BucketName bucketName);
Publisher<Boolean> delete(BucketName bucketName, BlobId blobId);
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
index f0e2844..5288284 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
@@ -99,4 +99,6 @@
* otherwise an IOObjectStoreException in its error channel
*/
Publisher<Void> deleteBucket(BucketName bucketName);
+
+ Publisher<BucketName> listBuckets();
}
diff --git a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
index 8c57fec..4b104ac 100644
--- a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
+++ b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/MetricableBlobStore.java
@@ -102,4 +102,9 @@
return metricFactory.decoratePublisherWithTimerMetric(DELETE_TIMER_NAME, blobStoreImpl.delete(bucketName, blobId));
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return blobStoreImpl.listBuckets();
+ }
}
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
index 75b0706..e616ee2 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreContract.java
@@ -31,6 +31,7 @@
import org.apache.james.util.concurrency.ConcurrentTestRunner;
import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface BucketBlobStoreContract {
@@ -168,4 +169,33 @@
.operationCount(10)
.runSuccessfullyWithin(Duration.ofMinutes(1));
}
+
+ @Test
+ default void listBucketsShouldReturnDefaultBucket() {
+ BlobStore store = testee();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(store.getDefaultBucketName());
+ }
+
+ @Test
+ default void listBucketsShouldReturnACustomBucket() {
+ BlobStore store = testee();
+
+ Mono.from(store.save(CUSTOM, SHORT_BYTEARRAY, LOW_COST)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(store.getDefaultBucketName(), CUSTOM);
+ }
+
+ @Test
+ default void listBucketsShouldNotReturnADeletedBucket() {
+ BlobStore store = testee();
+
+ Mono.from(store.save(CUSTOM, SHORT_BYTEARRAY, LOW_COST)).block();
+ Mono.from(store.deleteBucket(CUSTOM)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(store.getDefaultBucketName());
+ }
}
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
index acb4fe8..eb71a37 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/BucketBlobStoreDAOContract.java
@@ -35,6 +35,7 @@
import org.apache.james.util.concurrency.ConcurrentTestRunner;
import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface BucketBlobStoreDAOContract {
@@ -172,4 +173,68 @@
.operationCount(10)
.runSuccessfullyWithin(Duration.ofMinutes(1));
}
+
+ @Test
+ default void listBucketsShouldReturnEmptyWhenNone() {
+ BlobStoreDAO store = testee();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .isEmpty();
+ }
+
+ @Test
+ default void listBucketsShouldReturnBucketInUse() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(TEST_BUCKET_NAME);
+ }
+
+ @Test
+ default void listBucketsShouldNotReturnDuplicates() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .hasSize(1);
+ }
+
+ @Test
+ default void listBucketsShouldReturnAllBucketsInUse() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+ Mono.from(store.save(CUSTOM_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(TEST_BUCKET_NAME, CUSTOM_BUCKET_NAME);
+ }
+
+ @Test
+ default void listBucketsShouldNotReturnDeletedBuckets() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ Mono.from(store.deleteBucket(TEST_BUCKET_NAME)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .isEmpty();
+ }
+
+ @Test
+ default void listBucketsShouldReturnBucketsWithNoBlob() {
+ BlobStoreDAO store = testee();
+
+ Mono.from(store.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ Mono.from(store.delete(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertThat(Flux.from(store.listBuckets()).collectList().block())
+ .containsOnly(TEST_BUCKET_NAME);
+ }
}
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
index ffcc179..b27ec29 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAO.java
@@ -42,6 +42,7 @@
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.util.DataChunker;
import org.apache.james.util.ReactorUtils;
+import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -69,7 +70,6 @@
private final CassandraConfiguration configuration;
private final BucketName defaultBucket;
- private final MetricFactory metricFactory;
private final Metric metricClOneHitCount;
private final Metric metricClOneMissCount;
@@ -84,7 +84,6 @@
this.bucketDAO = bucketDAO;
this.configuration = cassandraConfiguration;
this.defaultBucket = defaultBucket;
- this.metricFactory = metricFactory;
this.metricClOneMissCount = metricFactory.generate(CASSANDRA_BLOBSTORE_CL_ONE_MISS_COUNT_METRIC_NAME);
this.metricClOneHitCount = metricFactory.generate(CASSANDRA_BLOBSTORE_CL_ONE_HIT_COUNT_METRIC_NAME);
@@ -269,4 +268,11 @@
.reduce(ByteBuffer.allocate(targetSize), ByteBuffer::put)
.array();
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return bucketDAO.listAll()
+ .map(Pair::getLeft)
+ .distinct();
+ }
}
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
index 9b6d0b9..fb74113 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CachedBlobStore.java
@@ -322,4 +322,9 @@
return Mono.from(metricFactory.decoratePublisherWithTimerMetric(BLOBSTORE_BACKEND_LATENCY_METRIC_NAME,
backend.readBytes(bucketName, blobId)));
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return backend.listBuckets();
+ }
}
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
index 6b2ad1d..159d5a1 100644
--- a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreDAOTest.java
@@ -28,11 +28,11 @@
import org.apache.james.blob.api.HashBlobId;
import org.apache.james.metrics.tests.RecordingMetricFactory;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.RegisterExtension;
public class CassandraBlobStoreDAOTest implements BlobStoreDAOContract {
private static final int CHUNK_SIZE = 10240;
- private static final int MULTIPLE_CHUNK_SIZE = 3;
@RegisterExtension
static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraBlobModule.MODULE);
@@ -60,4 +60,9 @@
return testee;
}
+ @Override
+ @Disabled("Not supported by the Cassandra blob store")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+ }
}
\ No newline at end of file
diff --git a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
index f66cc1f..95b256f 100644
--- a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
+++ b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
@@ -29,12 +29,14 @@
import org.apache.james.blob.api.BucketName;
import org.apache.james.blob.api.ObjectNotFoundException;
import org.apache.james.blob.api.ObjectStoreIOException;
+import org.reactivestreams.Publisher;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.io.ByteSource;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class MemoryBlobStoreDAO implements BlobStoreDAO {
@@ -110,4 +112,9 @@
}
});
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return Flux.fromIterable(blobs.rowKeySet());
+ }
}
diff --git a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
index 3225301..83748a7 100644
--- a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
+++ b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreDAOTest.java
@@ -22,6 +22,7 @@
import org.apache.james.blob.api.BlobStoreDAO;
import org.apache.james.blob.api.BlobStoreDAOContract;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
class MemoryBlobStoreDAOTest implements BlobStoreDAOContract {
@@ -36,4 +37,10 @@
public BlobStoreDAO testee() {
return blobStore;
}
+
+ @Override
+ @Disabled("Not supported")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+
+ }
}
diff --git a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
index a0fcb71..7b13ad3 100644
--- a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
+++ b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/BucketNameResolver.java
@@ -95,6 +95,19 @@
.orElse(bucketName);
}
+ Optional<BucketName> unresolve(BucketName bucketName) {
+ if (isNameSpace(bucketName)) {
+ return Optional.of(bucketName);
+ }
+
+ return prefix.map(p -> {
+ if (bucketName.asString().startsWith(p)) {
+ return Optional.of(BucketName.of(bucketName.asString().substring(p.length())));
+ }
+ return Optional.<BucketName>empty();
+ }).orElse(Optional.of(bucketName));
+ }
+
private boolean isNameSpace(BucketName bucketName) {
return namespace
.map(existingNamespace -> existingNamespace.equals(bucketName))
diff --git a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
index 5849bf9..24a35a4 100644
--- a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
+++ b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
@@ -41,6 +41,7 @@
import org.apache.james.lifecycle.api.Startable;
import org.apache.james.util.DataChunker;
import org.apache.james.util.ReactorUtils;
+import org.reactivestreams.Publisher;
import com.github.fge.lambdas.Throwing;
import com.github.steveash.guavate.Guavate;
@@ -63,11 +64,11 @@
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.Bucket;
import software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException;
import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.ListBucketsResponse;
-import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
@@ -279,10 +280,13 @@
private Mono<BucketName> emptyBucket(BucketName bucketName) {
return Mono.fromFuture(() -> client.listObjects(builder -> builder.bucket(bucketName.asString())))
- .flatMapIterable(ListObjectsResponse::contents)
- .window(EMPTY_BUCKET_BATCH_SIZE)
- .flatMap(this::buildListForBatch, DEFAULT_CONCURRENCY)
- .flatMap(identifiers -> deleteObjects(bucketName, identifiers), DEFAULT_CONCURRENCY)
+ .flatMap(response -> Flux.fromIterable(response.contents())
+ .window(EMPTY_BUCKET_BATCH_SIZE)
+ .flatMap(this::buildListForBatch, DEFAULT_CONCURRENCY)
+ .flatMap(identifiers -> deleteObjects(bucketName, identifiers), DEFAULT_CONCURRENCY)
+ .then(Mono.just(response)))
+ .flux()
+ .takeUntil(list -> !list.isTruncated())
.then(Mono.just(bucketName));
}
@@ -305,4 +309,13 @@
.flatMap(bucket -> deleteResolvedBucket(BucketName.of(bucket.name())), DEFAULT_CONCURRENCY)
.then();
}
+
+ @Override
+ public Publisher<BucketName> listBuckets() {
+ return Mono.fromFuture(client::listBuckets)
+ .flatMapIterable(ListBucketsResponse::buckets)
+ .map(Bucket::name)
+ .handle((bucket, sink) -> bucketNameResolver.unresolve(BucketName.of(bucket))
+ .ifPresent(sink::next));
+ }
}
diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
index 43e9e84..178830d 100644
--- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
+++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/BucketNameResolverTest.java
@@ -25,10 +25,25 @@
import org.apache.james.blob.api.BucketName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
class BucketNameResolverTest {
@Nested
class EmptyPrefix {
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValue() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -50,10 +65,45 @@
assertThat(resolver.resolve(BucketName.of("namespace")))
.isEqualTo(BucketName.of("namespace"));
}
+
+ @Test
+ void unresolveShouldReturnPassedValue() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
+
+ @Test
+ void unresolveShouldReturnValueWhenNamespace() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("namespace")))
+ .contains(BucketName.of("namespace"));
+ }
}
@Nested
class EmptyNamespace {
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .noNamespace()
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValueWithPrefix() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -64,10 +114,45 @@
assertThat(resolver.resolve(BucketName.of("bucketName")))
.isEqualTo(BucketName.of("bucketPrefix-bucketName"));
}
+
+ @Test
+ void unresolveShouldReturnPassedValueWithPrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .noNamespace()
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketPrefix-bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
+
+ @Test
+ void unresolveShouldFilterValuesWithoutPrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .noNamespace()
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .isEmpty();
+ }
}
@Nested
class BothAreEmpty {
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .noNamespace()
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValue() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -78,10 +163,35 @@
assertThat(resolver.resolve(BucketName.of("bucketName")))
.isEqualTo(BucketName.of("bucketName"));
}
+
+ @Test
+ void unresolveShouldReturnPassedValue() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .noPrefix()
+ .noNamespace()
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
}
@Nested
class BothArePresent {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"namespace", "any", "bucketPrefix-aaa", "bucketPrefix-"})
+ void withShouldAddNewValuesInSet(String bucketNameString) {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ BucketName bucketName = BucketName.of(bucketNameString);
+ assertThat(resolver.unresolve(resolver.resolve(bucketName)))
+ .contains(bucketName);
+ }
+
@Test
void resolveShouldReturnPassedValueWithPrefix() {
BucketNameResolver resolver = BucketNameResolver.builder()
@@ -103,6 +213,39 @@
assertThat(resolver.resolve(BucketName.of("namespace")))
.isEqualTo(BucketName.of("namespace"));
}
+
+ @Test
+ void unresolveShouldFilterValuesWithoutPrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketName")))
+ .isEmpty();
+ }
+
+ @Test
+ void unresolveShouldRemovePrefix() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("bucketPrefix-bucketName")))
+ .contains(BucketName.of("bucketName"));
+ }
+
+ @Test
+ void unresolveShouldReturnNamespaceWhenPassingNamespace() {
+ BucketNameResolver resolver = BucketNameResolver.builder()
+ .prefix("bucketPrefix-")
+ .namespace(BucketName.of("namespace"))
+ .build();
+
+ assertThat(resolver.unresolve(BucketName.of("namespace")))
+ .contains(BucketName.of("namespace"));
+ }
}
diff --git a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
index 618beb4..15148c2 100644
--- a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
+++ b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/DeDuplicationBlobStore.scala
@@ -29,7 +29,7 @@
import org.apache.commons.io.IOUtils
import org.apache.james.blob.api.{BlobId, BlobStore, BlobStoreDAO, BucketName}
import org.reactivestreams.Publisher
-import reactor.core.publisher.Mono
+import reactor.core.publisher.{Flux, Mono}
import reactor.core.scala.publisher.SMono
import reactor.util.function.{Tuple2, Tuples}
@@ -112,4 +112,6 @@
SMono.just(Boolean.box(false))
}
+
+ override def listBuckets(): Publisher[BucketName] = Flux.concat(blobStoreDAO.listBuckets(), Flux.just(defaultBucketName)).distinct()
}
diff --git a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
index 6cb73e4..faf4a5d 100644
--- a/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
+++ b/server/blob/blob-storage-strategy/src/main/scala/org/apache/james/server/blob/deduplication/PassThroughBlobStore.scala
@@ -26,6 +26,7 @@
import javax.inject.{Inject, Named}
import org.apache.james.blob.api.{BlobId, BlobStore, BlobStoreDAO, BucketName}
import org.reactivestreams.Publisher
+import reactor.core.publisher.Flux
import reactor.core.scala.publisher.SMono
@@ -86,4 +87,6 @@
SMono.fromPublisher(blobStoreDAO.delete(bucketName, blobId))
.`then`(SMono.just(Boolean.box(true)))
}
+
+ override def listBuckets(): Publisher[BucketName] = Flux.concat(blobStoreDAO.listBuckets(), Flux.just(defaultBucketName)).distinct()
}
diff --git a/server/container/guice/data-ldap/pom.xml b/server/container/guice/data-ldap/pom.xml
index 5bc6f60..752804c 100644
--- a/server/container/guice/data-ldap/pom.xml
+++ b/server/container/guice/data-ldap/pom.xml
@@ -43,10 +43,23 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-data-ldap</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-common</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-common</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-testing</artifactId>
<scope>test</scope>
</dependency>
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/DockerLdapRule.java b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/DockerLdapRule.java
similarity index 96%
rename from server/apps/cassandra-app/src/test/java/org/apache/james/DockerLdapRule.java
rename to server/container/guice/data-ldap/src/test/java/org/apache/james/data/DockerLdapRule.java
index 4ce36af..2cdada9 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/DockerLdapRule.java
+++ b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/DockerLdapRule.java
@@ -17,9 +17,10 @@
* under the License. *
****************************************************************/
-package org.apache.james;
+package org.apache.james.data;
import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.james.GuiceModuleTestRule;
import org.apache.james.user.ldap.DockerLdapSingleton;
import org.apache.james.user.ldap.LdapRepositoryConfiguration;
import org.junit.runner.Description;
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/LdapTestExtension.java
similarity index 91%
rename from server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
rename to server/container/guice/data-ldap/src/test/java/org/apache/james/data/LdapTestExtension.java
index a468116..d64312f 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
+++ b/server/container/guice/data-ldap/src/test/java/org/apache/james/data/LdapTestExtension.java
@@ -17,8 +17,9 @@
* under the License. *
****************************************************************/
-package org.apache.james;
+package org.apache.james.data;
+import org.apache.james.GuiceModuleTestExtension;
import org.junit.jupiter.api.extension.ExtensionContext;
import com.google.inject.Module;
@@ -27,11 +28,11 @@
private DockerLdapRule ldapRule;
- LdapTestExtension() {
+ public LdapTestExtension() {
this(new DockerLdapRule());
}
- LdapTestExtension(DockerLdapRule ldapRule) {
+ public LdapTestExtension(DockerLdapRule ldapRule) {
this.ldapRule = ldapRule;
}
diff --git a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java
index ee79def..4082070 100644
--- a/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java
+++ b/server/container/guice/jpa-common/src/main/java/org/apache/james/modules/data/JPADataModule.java
@@ -21,10 +21,8 @@
import com.google.inject.AbstractModule;
public class JPADataModule extends AbstractModule {
-
@Override
protected void configure() {
- install(new JPAUsersRepositoryModule());
install(new JPADomainListModule());
install(new JPARecipientRewriteTableModule());
install(new JPAMailRepositoryModule());
diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
index 10755c1..56e31fd 100644
--- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
+++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java
@@ -173,6 +173,13 @@
return messageManager.appendMessage(appendCommand, mailboxSession).getId();
}
+ public MessageManager.AppendResult appendMessageAndGetAppendResult(String username, MailboxPath mailboxPath, MessageManager.AppendCommand appendCommand)
+ throws MailboxException {
+ MailboxSession mailboxSession = mailboxManager.createSystemSession(Username.of(username));
+ MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, mailboxSession);
+ return messageManager.appendMessage(appendCommand, mailboxSession);
+ }
+
@Override
public Collection<String> listSubscriptions(String user) throws Exception {
MailboxSession mailboxSession = mailboxManager.createSystemSession(Username.of(user));
diff --git a/server/container/guice/pom.xml b/server/container/guice/pom.xml
index ea45f6f..dea05b8 100644
--- a/server/container/guice/pom.xml
+++ b/server/container/guice/pom.xml
@@ -104,6 +104,12 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-guice-data-ldap</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-guice-distributed</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
index 1cb9130..3c29347 100644
--- a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
+++ b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
@@ -19,9 +19,13 @@
package org.apache.james.user.cassandra;
+import java.util.Optional;
+
import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.core.Username;
import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.lib.UsersRepositoryContract;
import org.apache.james.user.lib.UsersRepositoryImpl;
import org.apache.james.user.lib.model.Algorithm;
@@ -39,17 +43,24 @@
@RegisterExtension
UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
- private UsersRepositoryImpl usersRepository;
+ private UsersRepositoryImpl<CassandraUsersDAO> usersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) throws Exception {
- usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+ usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+ this.testSystem = testSystem;
}
@Override
- public UsersRepositoryImpl testee() {
+ public UsersRepositoryImpl<CassandraUsersDAO> testee() {
return usersRepository;
}
+
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+ }
}
@Nested
@@ -57,25 +68,33 @@
@RegisterExtension
UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
- private UsersRepositoryImpl usersRepository;
+ private UsersRepositoryImpl<CassandraUsersDAO> usersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) throws Exception {
- usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+ usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+ this.testSystem = testSystem;
}
@Override
- public UsersRepositoryImpl testee() {
+ public UsersRepositoryImpl<CassandraUsersDAO> testee() {
return usersRepository;
}
+
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+ }
}
- private static UsersRepositoryImpl getUsersRepository(DomainList domainList, boolean enableVirtualHosting) throws Exception {
+ private static UsersRepositoryImpl<CassandraUsersDAO> getUsersRepository(DomainList domainList, boolean enableVirtualHosting, Optional<Username> administrator) throws Exception {
CassandraUsersDAO usersDAO = new CassandraUsersDAO(new Algorithm.DefaultFactory(), cassandraCluster.getCassandraCluster().getConf());
BaseHierarchicalConfiguration configuration = new BaseHierarchicalConfiguration();
configuration.addProperty("enableVirtualHosting", String.valueOf(enableVirtualHosting));
+ administrator.ifPresent(username -> configuration.addProperty("administratorId", username.asString()));
- UsersRepositoryImpl usersRepository = new UsersRepositoryImpl(domainList, usersDAO);
+ UsersRepositoryImpl<CassandraUsersDAO> usersRepository = new UsersRepositoryImpl<>(domainList, usersDAO);
usersRepository.configure(configuration);
return usersRepository;
}
diff --git a/server/data/data-jmap/pom.xml b/server/data/data-jmap/pom.xml
index 8456e56..c5972d8 100644
--- a/server/data/data-jmap/pom.xml
+++ b/server/data/data-jmap/pom.xml
@@ -45,6 +45,20 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>blob-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>blob-memory</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>blob-storage-strategy</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>event-sourcing-core</artifactId>
</dependency>
<dependency>
@@ -85,6 +99,11 @@
<artifactId>guava</artifactId>
</dependency>
<dependency>
+ <groupId>eu.timepit</groupId>
+ <artifactId>refined_${scala.base}</artifactId>
+ <version>0.9.20</version>
+ </dependency>
+ <dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
@@ -112,4 +131,13 @@
</dependency>
</dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>net.alchim31.maven</groupId>
+ <artifactId>scala-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
</project>
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Size.scala b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Size.scala
new file mode 100644
index 0000000..f4ed8d3
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Size.scala
@@ -0,0 +1,41 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.api.model
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.auto._
+import eu.timepit.refined.numeric.NonNegative
+import eu.timepit.refined.refineV
+import org.slf4j.{Logger, LoggerFactory}
+
+object Size {
+ private val logger: Logger = LoggerFactory.getLogger(classOf[Size])
+ type Size = Long Refined NonNegative
+ val Zero: Size = 0L
+
+ def sanitizeSize(value: Long): Size = {
+ val size: Either[String, Size] = refineV[NonNegative](value)
+ size.fold(e => {
+ logger.error(s"Encountered an invalid size: $e")
+ Zero
+ },
+ refinedValue => refinedValue)
+ }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Upload.scala b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Upload.scala
new file mode 100644
index 0000000..73e8578
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Upload.scala
@@ -0,0 +1,56 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.api.model
+
+import org.apache.james.blob.api.BlobId
+import org.apache.james.jmap.api.model.Size.Size
+import org.apache.james.mailbox.model.ContentType
+
+import java.io.InputStream
+
+object Upload {
+
+ def from(metaData: UploadMetaData, content: InputStream): Upload =
+ Upload(uploadId = metaData.uploadId,
+ size = metaData.size,
+ contentType = metaData.contentType,
+ content = content)
+}
+
+case class Upload(uploadId: UploadId,
+ size: Size,
+ contentType: ContentType,
+ content: InputStream)
+
+case class UploadNotFoundException(uploadId: UploadId) extends RuntimeException(s"Upload not found $uploadId")
+
+object UploadMetaData {
+ def from(uploadId: UploadId, contentType: ContentType, size: Long, blobId: BlobId): UploadMetaData =
+ UploadMetaData(uploadId = uploadId,
+ contentType = contentType,
+ size = Size.sanitizeSize(size),
+ blobId = blobId)
+}
+
+case class UploadMetaData(uploadId: UploadId,
+ contentType: ContentType,
+ size: Size,
+ blobId: BlobId)
+
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/UploadId.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/UploadId.java
new file mode 100644
index 0000000..7d58646
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/UploadId.java
@@ -0,0 +1,75 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.api.model;
+
+import org.apache.commons.text.RandomStringGenerator;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+public class UploadId {
+ public static final RandomStringGenerator RANDOM_STRING_GENERATOR = new RandomStringGenerator.Builder()
+ .withinRange('a', 'z')
+ .build();
+
+ public static UploadId random() {
+ return new UploadId(RANDOM_STRING_GENERATOR.generate(20));
+ }
+
+ public static UploadId from(String id) {
+ Preconditions.checkNotNull(id);
+ Preconditions.checkArgument(!id.isEmpty());
+ return new UploadId(id);
+ }
+
+ private final String id;
+
+ public UploadId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof UploadId) {
+ UploadId other = (UploadId) obj;
+ return Objects.equal(id, other.id);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects
+ .toStringHelper(this)
+ .add("id", id)
+ .toString();
+ }
+
+}
diff --git a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadRepository.java
similarity index 60%
copy from server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
copy to server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadRepository.java
index a468116..b104037 100644
--- a/server/apps/cassandra-app/src/test/java/org/apache/james/LdapTestExtension.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadRepository.java
@@ -17,40 +17,19 @@
* under the License. *
****************************************************************/
-package org.apache.james;
+package org.apache.james.jmap.api.upload;
-import org.junit.jupiter.api.extension.ExtensionContext;
+import java.io.InputStream;
-import com.google.inject.Module;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.Upload;
+import org.apache.james.jmap.api.model.UploadId;
+import org.apache.james.mailbox.model.ContentType;
+import org.reactivestreams.Publisher;
-public class LdapTestExtension implements GuiceModuleTestExtension {
+public interface UploadRepository {
+ Publisher<UploadId> upload(InputStream data, ContentType contentType, Username user);
- private DockerLdapRule ldapRule;
-
- LdapTestExtension() {
- this(new DockerLdapRule());
- }
-
- LdapTestExtension(DockerLdapRule ldapRule) {
- this.ldapRule = ldapRule;
- }
-
- @Override
- public void beforeAll(ExtensionContext extensionContext) {
- ldapRule.start();
- }
-
- @Override
- public void afterAll(ExtensionContext extensionContext) {
- ldapRule.stop();
- }
-
- @Override
- public Module getModule() {
- return ldapRule.getModule();
- }
-
- public DockerLdapRule getLdapRule() {
- return ldapRule;
- }
+ Publisher<Upload> retrieve(UploadId id, Username user);
}
+
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepository.java
new file mode 100644
index 0000000..256409a
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepository.java
@@ -0,0 +1,99 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.memory.upload;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.model.Upload;
+import org.apache.james.jmap.api.model.UploadId;
+import org.apache.james.jmap.api.model.UploadMetaData;
+import org.apache.james.jmap.api.model.UploadNotFoundException;
+import org.apache.james.jmap.api.upload.UploadRepository;
+import org.apache.james.mailbox.model.ContentType;
+import org.reactivestreams.Publisher;
+
+import com.google.common.base.Preconditions;
+
+import reactor.core.publisher.Mono;
+
+public class InMemoryUploadRepository implements UploadRepository {
+
+ private static final Map<UploadId, ImmutablePair<Username, UploadMetaData>> uploadStore = new HashMap<>();
+
+ private final BlobStore blobStore;
+ private final BucketName bucketName;
+
+ @Inject
+ public InMemoryUploadRepository(BlobStore blobStore) {
+ this.blobStore = blobStore;
+ this.bucketName = blobStore.getDefaultBucketName();
+ }
+
+ @Override
+ public Publisher<UploadId> upload(InputStream data, ContentType contentType, Username user) {
+ Preconditions.checkNotNull(data);
+ Preconditions.checkNotNull(contentType);
+ Preconditions.checkNotNull(user);
+
+ byte[] dataAsByte = toByteArray(data);
+ return Mono.from(blobStore.save(bucketName, dataAsByte, BlobStore.StoragePolicy.LOW_COST))
+ .map(blobId -> {
+ UploadId uploadId = UploadId.random();
+ uploadStore.put(uploadId, new ImmutablePair<>(user, UploadMetaData.from(uploadId, contentType, dataAsByte.length, blobId)));
+ return uploadId;
+ });
+ }
+
+ @Override
+ public Publisher<Upload> retrieve(UploadId id, Username user) {
+ Preconditions.checkNotNull(id);
+ Preconditions.checkNotNull(user);
+
+ return Mono.justOrEmpty(uploadStore.get(id))
+ .filter(pair -> user.equals(pair.left))
+ .flatMap(userAndMetaData -> retrieveUpload(userAndMetaData.right))
+ .switchIfEmpty(Mono.error(() -> new UploadNotFoundException(id)));
+ }
+
+ private Mono<Upload> retrieveUpload(UploadMetaData uploadMetaData) {
+ return Mono.from(blobStore.readBytes(bucketName, uploadMetaData.blobId()))
+ .map(content -> Upload.from(uploadMetaData, new ByteArrayInputStream(content)));
+ }
+
+ private byte[] toByteArray(InputStream inputStream) {
+ try {
+ return IOUtils.toByteArray(inputStream);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadRepositoryContract.scala b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadRepositoryContract.scala
new file mode 100644
index 0000000..2bb529b
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadRepositoryContract.scala
@@ -0,0 +1,101 @@
+ /***************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+ package org.apache.james.jmap.api.upload
+
+ import org.apache.commons.io.IOUtils
+ import org.apache.james.core.Username
+ import org.apache.james.jmap.api.model.Size.sanitizeSize
+ import org.apache.james.jmap.api.model.{Upload, UploadId, UploadNotFoundException}
+ import org.apache.james.jmap.api.upload.UploadRepositoryContract.{CONTENT_TYPE, DATA_STRING, USER}
+ import org.apache.james.mailbox.model.ContentType
+ import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}
+ import org.junit.jupiter.api.Test
+ import reactor.core.scala.publisher.SMono
+
+ import java.io.InputStream
+ import java.nio.charset.StandardCharsets
+
+ object UploadRepositoryContract {
+ private lazy val CONTENT_TYPE: ContentType = ContentType
+ .of("text/html")
+ private lazy val DATA_STRING: String = "123321"
+ private lazy val USER: Username = Username.of("Bob")
+ }
+
+ trait UploadRepositoryContract {
+
+ def testee: UploadRepository
+
+ def data(): InputStream = IOUtils.toInputStream(DATA_STRING, StandardCharsets.UTF_8)
+
+ @Test
+ def uploadShouldSuccess(): Unit = {
+ val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+ assertThat(SMono.fromPublisher(testee.retrieve(uploadId, USER)).block())
+ .isNotNull
+ }
+
+ @Test
+ def uploadShouldReturnDifferentIdWhenDifferentData(): Unit = {
+ val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+ assertThat(uploadId)
+ .isNotEqualTo(SMono.fromPublisher(testee.upload(IOUtils.toInputStream("abcxyz", StandardCharsets.UTF_8), CONTENT_TYPE, USER)).block())
+ }
+
+ @Test
+ def uploadSameContentShouldReturnDifferentId(): Unit = {
+ val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+ assertThat(uploadId)
+ .isNotEqualTo(SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block())
+ }
+
+ @Test
+ def retrieveShouldSuccess(): Unit = {
+ val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+ val actualUpload: Upload = SMono.fromPublisher(testee.retrieve(uploadId, USER)).block()
+
+ assertThat(actualUpload.uploadId)
+ .isEqualTo(uploadId)
+ assertThat(actualUpload.contentType)
+ .isEqualTo(CONTENT_TYPE)
+ assertThat(actualUpload.size)
+ .isEqualTo(sanitizeSize(DATA_STRING.length))
+ assertThat(actualUpload.content.readAllBytes())
+ .isEqualTo(DATA_STRING.getBytes)
+ }
+
+ @Test
+ def retrieveShouldThrowWhenUploadIdIsNotExist(): Unit = {
+ assertThatThrownBy(() => SMono.fromPublisher(testee.retrieve(UploadId.from("notFoundId"), USER)).block())
+ .isInstanceOf(classOf[UploadNotFoundException])
+ }
+
+ @Test
+ def retrieveShouldThrowWhenUserIsNotOwnerOfUpload(): Unit = {
+ val uploadId: UploadId = SMono.fromPublisher(testee.upload(data(), CONTENT_TYPE, USER)).block()
+
+ assertThatThrownBy(() => SMono.fromPublisher(testee.retrieve(uploadId, Username.of("Alice"))).block())
+ .isInstanceOf(classOf[UploadNotFoundException])
+ }
+
+ }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepositoryTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepositoryTest.java
new file mode 100644
index 0000000..a863354
--- /dev/null
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadRepositoryTest.java
@@ -0,0 +1,45 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.memory.upload;
+
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.jmap.api.upload.UploadRepository;
+import org.apache.james.jmap.api.upload.UploadRepositoryContract;
+import org.junit.jupiter.api.BeforeEach;
+import org.apache.james.blob.memory.MemoryBlobStoreDAO;
+import org.apache.james.server.blob.deduplication.DeDuplicationBlobStore;
+
+public class InMemoryUploadRepositoryTest implements UploadRepositoryContract {
+
+ private UploadRepository testee;
+
+ @BeforeEach
+ void setUp() {
+ BlobStore blobStore = new DeDuplicationBlobStore(new MemoryBlobStoreDAO(), BucketName.DEFAULT, new HashBlobId.Factory());
+ testee = new InMemoryUploadRepository(blobStore);
+ }
+
+ @Override
+ public UploadRepository testee() {
+ return testee;
+ }
+}
diff --git a/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java b/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
index 9ef1078..55355b0 100644
--- a/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
+++ b/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
@@ -18,12 +18,15 @@
****************************************************************/
package org.apache.james.user.jpa;
+import java.util.Optional;
+
import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
import org.apache.james.backends.jpa.JpaTestCluster;
+import org.apache.james.core.Username;
import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.jpa.model.JPAUser;
import org.apache.james.user.lib.UsersRepositoryContract;
-import org.apache.james.user.lib.UsersRepositoryImpl;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -39,16 +42,23 @@
UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
private JPAUsersRepository usersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) throws Exception {
- usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+ usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+ this.testSystem = testSystem;
}
@Override
- public UsersRepositoryImpl testee() {
+ public UsersRepository testee() {
return usersRepository;
}
+
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+ }
}
@Nested
@@ -57,16 +67,23 @@
UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
private JPAUsersRepository usersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) throws Exception {
- usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting());
+ usersRepository = getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), Optional.empty());
+ this.testSystem = testSystem;
}
@Override
- public UsersRepositoryImpl testee() {
+ public UsersRepository testee() {
return usersRepository;
}
+
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ return getUsersRepository(testSystem.getDomainList(), extension.isSupportVirtualHosting(), administrator);
+ }
}
@AfterEach
@@ -74,11 +91,12 @@
JPA_TEST_CLUSTER.clear("JAMES_USER");
}
- private static JPAUsersRepository getUsersRepository(DomainList domainList, boolean enableVirtualHosting) throws Exception {
+ private static JPAUsersRepository getUsersRepository(DomainList domainList, boolean enableVirtualHosting, Optional<Username> administrator) throws Exception {
JPAUsersRepository repos = new JPAUsersRepository(domainList);
repos.setEntityManagerFactory(JPA_TEST_CLUSTER.getEntityManagerFactory());
BaseHierarchicalConfiguration configuration = new BaseHierarchicalConfiguration();
configuration.addProperty("enableVirtualHosting", String.valueOf(enableVirtualHosting));
+ administrator.ifPresent(username -> configuration.addProperty("administratorId", username.asString()));
repos.configure(configuration);
return repos;
}
diff --git a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
index 9638dbf..a24c698 100644
--- a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
+++ b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
@@ -28,6 +28,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.util.Optional;
+
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConversionException;
import org.apache.commons.configuration2.plist.PropertyListConfiguration;
@@ -35,6 +37,7 @@
import org.apache.james.core.Username;
import org.apache.james.domainlist.api.DomainList;
import org.apache.james.domainlist.api.mock.SimpleDomainList;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.lib.UsersRepositoryContract;
import org.apache.james.user.lib.UsersRepositoryImpl;
import org.junit.jupiter.api.AfterAll;
@@ -142,10 +145,12 @@
UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
private ReadOnlyUsersLDAPRepository usersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) throws Exception {
usersRepository = startUsersRepository(ldapRepositoryConfigurationWithVirtualHosting(ldapContainer), testSystem.getDomainList());
+ this.testSystem = testSystem;
}
@Override
@@ -153,6 +158,11 @@
return usersRepository;
}
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ return startUsersRepository(ldapRepositoryConfigurationWithVirtualHosting(ldapContainer, administrator), testSystem.getDomainList());
+ }
+
@Test
void isAdministratorShouldReturnTrueWhenConfiguredAndUserIsAdmin(TestSystem testSystem) throws Exception {
assertThat(testee().isAdministrator(testSystem.getAdmin())).isTrue();
@@ -241,10 +251,12 @@
UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
private ReadOnlyUsersLDAPRepository usersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) throws Exception {
usersRepository = startUsersRepository(ldapRepositoryConfiguration(ldapContainer), testSystem.getDomainList());
+ this.testSystem = testSystem;
}
@Override
@@ -252,6 +264,11 @@
return usersRepository;
}
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ return startUsersRepository(ldapRepositoryConfiguration(ldapContainer, administrator), testSystem.getDomainList());
+ }
+
@Test
void knownUserShouldBeAbleToLogInWhenPasswordIsCorrect() throws Exception {
assertThat(usersRepository.test(JAMES_USER, PASSWORD)).isTrue();
@@ -371,17 +388,25 @@
}
static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfiguration(LdapGenericContainer ldapContainer) {
+ return ldapRepositoryConfiguration(ldapContainer, Optional.of(Username.of(ADMIN_LOCAL_PART)));
+ }
+
+ static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfiguration(LdapGenericContainer ldapContainer, Optional<Username> administrator) {
PropertyListConfiguration configuration = baseConfiguration(ldapContainer);
configuration.addProperty("[@userIdAttribute]", "uid");
- configuration.addProperty("[@administratorId]", ADMIN_LOCAL_PART);
+ administrator.ifPresent(username -> configuration.addProperty("[@administratorId]", username.asString()));
return configuration;
}
static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfigurationWithVirtualHosting(LdapGenericContainer ldapContainer) {
+ return ldapRepositoryConfigurationWithVirtualHosting(ldapContainer, Optional.of(ADMIN));
+ }
+
+ static HierarchicalConfiguration<ImmutableNode> ldapRepositoryConfigurationWithVirtualHosting(LdapGenericContainer ldapContainer, Optional<Username> administrator) {
PropertyListConfiguration configuration = baseConfiguration(ldapContainer);
configuration.addProperty("[@userIdAttribute]", "mail");
configuration.addProperty("supportsVirtualHosting", true);
- configuration.addProperty("[@administratorId]", ADMIN.asString());
+ administrator.ifPresent(username -> configuration.addProperty("[@administratorId]", username.asString()));
return configuration;
}
diff --git a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
index 15e0173..d499e74 100644
--- a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
+++ b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
@@ -40,7 +40,6 @@
import org.apache.james.user.api.model.User;
import org.slf4j.LoggerFactory;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
public class UsersRepositoryImpl<T extends UsersDAO> implements UsersRepository, Configurable {
@@ -164,10 +163,6 @@
return virtualHosting;
}
- @VisibleForTesting void setAdministratorId(Optional<Username> username) {
- this.administratorId = username;
- }
-
@Override
public boolean isAdministrator(Username username) throws UsersRepositoryException {
assertValid(username);
diff --git a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
index eabe6db..000b84a 100644
--- a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
+++ b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
@@ -34,6 +34,7 @@
import org.apache.james.domainlist.api.mock.SimpleDomainList;
import org.apache.james.user.api.AlreadyExistInUsersRepositoryException;
import org.apache.james.user.api.InvalidUsernameException;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
import org.apache.james.user.api.model.User;
import org.apache.james.user.lib.model.Algorithm;
@@ -53,15 +54,12 @@
class UserRepositoryExtension implements BeforeEachCallback, ParameterResolver {
- private static final boolean ENABLE_VIRTUAL_HOSTING = true;
- private static final boolean DISABLE_VIRTUAL_HOSTING = !ENABLE_VIRTUAL_HOSTING;
-
public static UserRepositoryExtension withVirtualHost() {
- return new UserRepositoryExtension(ENABLE_VIRTUAL_HOSTING);
+ return new UserRepositoryExtension(true);
}
public static UserRepositoryExtension withoutVirtualHosting() {
- return new UserRepositoryExtension(DISABLE_VIRTUAL_HOSTING);
+ return new UserRepositoryExtension(false);
}
private final boolean supportVirtualHosting;
@@ -93,7 +91,7 @@
class TestSystem {
static final Domain DOMAIN = Domain.of("james.org");
- static final Domain UNKNOW_DOMAIN = Domain.of("unknown.org");
+ static final Domain UNKNOWN_DOMAIN = Domain.of("unknown.org");
private final boolean supportVirtualHosting;
private final SimpleDomainList domainList;
@@ -103,7 +101,7 @@
private final Username user3;
private final Username admin;
private final Username adminCaseVariation;
- private final Username userWithUnknowDomain;
+ private final Username userWithUnknownDomain;
private final Username invalidUsername;
TestSystem(boolean supportVirtualHosting) throws Exception {
@@ -116,7 +114,7 @@
user1CaseVariation = toUsername("uSeRnaMe");
admin = toUsername("admin");
adminCaseVariation = toUsername("adMin");
- userWithUnknowDomain = toUsername("unknown", UNKNOW_DOMAIN);
+ userWithUnknownDomain = toUsername("unknown", UNKNOWN_DOMAIN);
invalidUsername = toUsername("userContains)*(");
}
@@ -140,12 +138,15 @@
return admin;
}
- public Username getUserWithUnknowDomain() {
- return userWithUnknowDomain;
+ public Username getUserWithUnknownDomain() {
+ return userWithUnknownDomain;
}
}
- UsersRepositoryImpl testee();
+ UsersRepository testee();
+
+ UsersRepository testee(Optional<Username> administrator) throws Exception;
+
interface ReadOnlyContract extends UsersRepositoryContract {
@Test
@@ -169,9 +170,9 @@
}
@Test
- default void isAdministratorShouldBeCaseInsentive(TestSystem testSystem) throws Exception {
- testee().setAdministratorId(Optional.of(testSystem.admin));
- assertThat(testee().isAdministrator(testSystem.adminCaseVariation))
+ default void isAdministratorShouldBeCaseInsensitive(TestSystem testSystem) throws Exception {
+ UsersRepository testee = testee(Optional.of(testSystem.admin));
+ assertThat(testee.isAdministrator(testSystem.adminCaseVariation))
.isTrue();
}
@@ -255,14 +256,14 @@
}
@Test
- default void containsShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+ default void containsShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
testee().addUser(testSystem.user1CaseVariation, "password2");
assertThat(testee().contains(testSystem.user1)).isTrue();
}
@Test
- default void containsShouldBeCaseInsentiveWhenOriginalValueLowerCased(TestSystem testSystem) throws UsersRepositoryException {
+ default void containsShouldBeCaseInsensitiveWhenOriginalValueLowerCased(TestSystem testSystem) throws UsersRepositoryException {
testee().addUser(testSystem.user1, "password2");
assertThat(testee().contains(testSystem.user1CaseVariation)).isTrue();
@@ -294,7 +295,7 @@
}
@Test
- default void removeUserShouldBeCaseInsentiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
+ default void removeUserShouldBeCaseInsensitiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
testee().addUser(testSystem.user1CaseVariation, "password2");
testee().removeUser(testSystem.user1);
@@ -305,7 +306,7 @@
}
@Test
- default void removeUserShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+ default void removeUserShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
testee().addUser(testSystem.user1, "password2");
testee().removeUser(testSystem.user1CaseVariation);
@@ -316,7 +317,7 @@
}
@Test
- default void getUserByNameShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+ default void getUserByNameShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
testee().addUser(testSystem.user1, "password2");
assertThat(testee().getUserByName(testSystem.user1CaseVariation).getUserName())
@@ -333,7 +334,7 @@
@Test
- default void testShouldBeCaseInsentiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
+ default void testShouldBeCaseInsensitiveOnCaseVariationUser(TestSystem testSystem) throws UsersRepositoryException {
String password = "password2";
testee().addUser(testSystem.user1CaseVariation, password);
@@ -342,7 +343,7 @@
}
@Test
- default void testShouldBeCaseInsentive(TestSystem testSystem) throws UsersRepositoryException {
+ default void testShouldBeCaseInsensitive(TestSystem testSystem) throws UsersRepositoryException {
String password = "password2";
testee().addUser(testSystem.user1, password);
@@ -516,23 +517,23 @@
@Test
default void isAdministratorShouldReturnFalseWhenNotConfigured(TestSystem testSystem) throws Exception {
- testee().setAdministratorId(Optional.empty());
+ UsersRepository testee = testee(Optional.empty());
- assertThat(testee().isAdministrator(testSystem.admin)).isFalse();
+ assertThat(testee.isAdministrator(testSystem.admin)).isFalse();
}
@Test
default void isAdministratorShouldReturnTrueWhenConfiguredAndUserIsAdmin(TestSystem testSystem) throws Exception {
- testee().setAdministratorId(Optional.of(testSystem.admin));
+ UsersRepository testee = testee(Optional.of(testSystem.admin));
- assertThat(testee().isAdministrator(testSystem.admin)).isTrue();
+ assertThat(testee.isAdministrator(testSystem.admin)).isTrue();
}
@Test
default void isAdministratorShouldReturnFalseWhenConfiguredAndUserIsNotAdmin(TestSystem testSystem) throws Exception {
- testee().setAdministratorId(Optional.of(testSystem.admin));
+ UsersRepository testee = testee(Optional.of(testSystem.admin));
- assertThat(testee().isAdministrator(testSystem.user1)).isFalse();
+ assertThat(testee.isAdministrator(testSystem.user1)).isFalse();
}
}
@@ -552,7 +553,7 @@
@Test
default void addUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatThrownBy(() -> testee().addUser(testSystem.userWithUnknowDomain, "password"))
+ assertThatThrownBy(() -> testee().addUser(testSystem.userWithUnknownDomain, "password"))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Domain does not exist in DomainList");
}
@@ -566,7 +567,7 @@
@Test
default void updateUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatThrownBy(() -> testee().updateUser(new DefaultUser(testSystem.userWithUnknowDomain, Algorithm.DEFAULT_FACTORY.of("hasAlg"))))
+ assertThatThrownBy(() -> testee().updateUser(new DefaultUser(testSystem.userWithUnknownDomain, Algorithm.DEFAULT_FACTORY.of("hasAlg"))))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Domain does not exist in DomainList");
}
@@ -579,7 +580,7 @@
@Test
default void removeUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatThrownBy(() -> testee().removeUser(testSystem.userWithUnknowDomain))
+ assertThatThrownBy(() -> testee().removeUser(testSystem.userWithUnknownDomain))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Domain does not exist in DomainList");
}
@@ -595,25 +596,25 @@
@Test
default void getUserByNameShouldNotThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatCode(() -> testee().getUserByName(testSystem.userWithUnknowDomain))
+ assertThatCode(() -> testee().getUserByName(testSystem.userWithUnknownDomain))
.doesNotThrowAnyException();
}
@Test
default void containsShouldNotThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatCode(() -> testee().contains(testSystem.userWithUnknowDomain))
+ assertThatCode(() -> testee().contains(testSystem.userWithUnknownDomain))
.doesNotThrowAnyException();
}
@Test
default void testShouldNotThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatCode(() -> testee().test(testSystem.userWithUnknowDomain, "password"))
+ assertThatCode(() -> testee().test(testSystem.userWithUnknownDomain, "password"))
.doesNotThrowAnyException();
}
@Test
default void isAdministratorShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
- assertThatThrownBy(() -> testee().isAdministrator(testSystem.userWithUnknowDomain))
+ assertThatThrownBy(() -> testee().isAdministrator(testSystem.userWithUnknownDomain))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Domain does not exist in DomainList");
}
@@ -646,14 +647,14 @@
default void assertDomainPartValidShouldThrowWhenDomainPartIsMissing() throws Exception {
Username withoutDomainPart = Username.fromLocalPartWithoutDomain("localPartOnly");
- assertThatThrownBy(() -> testee().assertDomainPartValid(withoutDomainPart))
+ assertThatThrownBy(() -> testee().assertValid(withoutDomainPart))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Given Username needs to contain a @domainpart");
}
@Test
default void assertDomainPartValidShouldThrowWhenDomainPartIsNotManaged(TestSystem testSystem) {
- assertThatThrownBy(() -> testee().assertDomainPartValid(testSystem.userWithUnknowDomain))
+ assertThatThrownBy(() -> testee().assertValid(testSystem.userWithUnknownDomain))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Domain does not exist in DomainList");
}
@@ -664,7 +665,7 @@
"localPart",
TestSystem.DOMAIN);
- assertThatCode(() -> testee().assertDomainPartValid(userWithManagedDomain))
+ assertThatCode(() -> testee().assertValid(userWithManagedDomain))
.doesNotThrowAnyException();
}
}
@@ -700,7 +701,7 @@
"localPart",
TestSystem.DOMAIN);
- assertThatThrownBy(() -> testee().assertDomainPartValid(withDomainPart))
+ assertThatThrownBy(() -> testee().assertValid(withDomainPart))
.isInstanceOf(InvalidUsernameException.class)
.hasMessage("Given Username contains a @domainpart but virtualhosting support is disabled");
}
@@ -709,7 +710,7 @@
default void assertDomainPartValidShouldNotThrowWhenDomainPartIsMissing() {
Username withOutDomainPart = Username.fromLocalPartWithoutDomain("localPartOnly");
- assertThatCode(() -> testee().assertDomainPartValid(withOutDomainPart))
+ assertThatCode(() -> testee().assertValid(withOutDomainPart))
.doesNotThrowAnyException();
}
}
diff --git a/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java b/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
index b77b437..28d8f32 100644
--- a/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
+++ b/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
@@ -22,14 +22,19 @@
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.util.Optional;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.james.core.Domain;
import org.apache.james.core.Username;
import org.apache.james.dnsservice.api.InMemoryDNSService;
import org.apache.james.domainlist.lib.DomainListConfiguration;
import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
import org.apache.james.user.lib.UsersRepositoryContract;
-import org.apache.james.user.lib.UsersRepositoryImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -46,14 +51,23 @@
UserRepositoryExtension extension = UserRepositoryExtension.withVirtualHost();
private MemoryUsersRepository memoryUsersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) {
memoryUsersRepository = MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+ this.testSystem = testSystem;
}
@Override
- public UsersRepositoryImpl testee() {
+ public UsersRepository testee() {
+ return memoryUsersRepository;
+ }
+
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ MemoryUsersRepository memoryUsersRepository = MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+ memoryUsersRepository.configure(configuration(administrator, true));
return memoryUsersRepository;
}
@@ -103,14 +117,23 @@
UserRepositoryExtension extension = UserRepositoryExtension.withoutVirtualHosting();
private MemoryUsersRepository memoryUsersRepository;
+ private TestSystem testSystem;
@BeforeEach
void setUp(TestSystem testSystem) {
memoryUsersRepository = MemoryUsersRepository.withoutVirtualHosting(testSystem.getDomainList());
+ this.testSystem = testSystem;
}
@Override
- public UsersRepositoryImpl testee() {
+ public UsersRepository testee() {
+ return memoryUsersRepository;
+ }
+
+ @Override
+ public UsersRepository testee(Optional<Username> administrator) throws Exception {
+ MemoryUsersRepository memoryUsersRepository = MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+ memoryUsersRepository.configure(configuration(administrator, false));
return memoryUsersRepository;
}
@@ -130,4 +153,12 @@
.doesNotThrowAnyException();
}
}
+
+ private HierarchicalConfiguration<ImmutableNode> configuration(Optional<Username> administrator, boolean enableVirtualHosting) {
+ BaseHierarchicalConfiguration configuration = new BaseHierarchicalConfiguration();
+ administrator.ifPresent(username -> configuration.addProperty("administratorId", username.asString()));
+
+ configuration.addProperty("enableVirtualHosting", enableVirtualHosting);
+ return configuration;
+ }
}
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java
index 31cd912..a8a6baf 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java
@@ -27,7 +27,6 @@
import org.apache.james.core.MailAddress;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
-import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
import org.apache.james.mailbox.model.SearchQuery;
@@ -106,7 +105,7 @@
.from(SearchQuery.of(SearchQuery.mimeMessageID(messageId)))
.build();
return Flux.from(mailboxManager.search(searchByRFC822MessageId, session, limit)).toStream().findFirst();
- } catch (MailboxException | UsersRepositoryException e) {
+ } catch (UsersRepositoryException e) {
LOGGER.error("unable to find message with Message-Id: " + messageId, e);
}
return Optional.empty();
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
index ae1eeb0..3811480 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/pom.xml
@@ -83,6 +83,13 @@
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <configuration>
+ <config>${project.parent.parent.parent.basedir}/.scalafix.conf</config>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
index a152b5e..d879bd6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
@@ -22,7 +22,6 @@
import java.io.{ByteArrayInputStream, InputStream}
import java.net.URI
import java.nio.charset.StandardCharsets
-
import com.google.inject.AbstractModule
import com.google.inject.multibindings.Multibinder
import eu.timepit.refined.auto._
@@ -31,6 +30,7 @@
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
import io.restassured.RestAssured._
import io.restassured.http.ContentType.JSON
+
import javax.inject.{Inject, Named}
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
import org.apache.http.HttpStatus.SC_OK
@@ -38,6 +38,7 @@
import org.apache.james.events.Event.EventId
import org.apache.james.events.EventBus
import org.apache.james.jmap.api.model.AccountId
+import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.change.{AccountIdRegistrationKey, StateChangeEvent, TypeName}
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE}
import org.apache.james.jmap.core.Invocation.MethodName
@@ -46,7 +47,6 @@
import org.apache.james.jmap.draft.JmapGuiceProbe
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.mail
-import org.apache.james.jmap.mail.Email.Size
import org.apache.james.jmap.method.{InvocationWithContext, Method}
import org.apache.james.jmap.rfc8621.contract.CustomMethodContract.CUSTOM
import org.apache.james.jmap.rfc8621.contract.DownloadContract.accountId
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala
index 59222f7..0ce6bee 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailChangesMethodContract.scala
@@ -210,15 +210,15 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build
val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
- val state1: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+ waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
- val state2: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+ waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
- val state3: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+ waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId4: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
- val state4: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+ waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId5: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
- val state5: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
+ waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
val messageId6: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val state6: State = waitForNextState(server, AccountId.fromUsername(BOB), State.INITIAL)
@@ -312,7 +312,7 @@
val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox1")
- val mailboxId1 = mailboxProbe.createMailbox(path)
+ mailboxProbe.createMailbox(path)
val mailboxId2 = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
val message: Message = Message.Builder
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index bcfdf8e..8968153 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -3198,7 +3198,7 @@
@Test
def receivedAtPropertyShouldBeReturned(server: GuiceJamesServer): Unit = {
val path = MailboxPath.inbox(BOB)
- val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
val message: Message = Message.Builder
.of
.setSubject("test")
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala
index e8716c5..18f00c4 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailImportContract.scala
@@ -672,7 +672,7 @@
@Test
def importShouldFailWhenBlobNotOwned(server: GuiceJamesServer): Unit = {
val andrePath = MailboxPath.inbox(ANDRE)
- val andreId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
val bobPath = MailboxPath.inbox(BOB)
val bobId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
val receivedAt = ZonedDateTime.now().minusDays(1)
@@ -743,7 +743,7 @@
@Test
def importShouldSucceedWhenBlobDelegated(server: GuiceJamesServer): Unit = {
val andrePath = MailboxPath.inbox(ANDRE)
- val andreId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
val bobPath = MailboxPath.inbox(BOB)
val bobId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
val receivedAt = ZonedDateTime.now().minusDays(1)
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
index 2b19d63..dd7efb8 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
@@ -147,7 +147,7 @@
def hasAttachmentShouldKeepMessageWithAttachmentWhenTrue(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB),
AppendCommand.from(
buildTestMessage))
@@ -459,8 +459,8 @@
@Test
def emailInSharedMailboxesShouldNotBeDisplayedWhenNoExtension(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe.createMailbox(inbox(ANDRE))
+ mailboxProbe
.appendMessage(ANDRE.asString, inbox(ANDRE),
AppendCommand.from(
Message.Builder
@@ -521,7 +521,7 @@
@Test
def emailInSharedMailboxesShouldBeDisplayedWhenExtension(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
+ mailboxProbe.createMailbox(inbox(ANDRE))
val messageId1: MessageId = mailboxProbe
.appendMessage(ANDRE.asString, inbox(ANDRE),
AppendCommand.from(
@@ -585,7 +585,7 @@
def inMailboxFilterShouldReturnEmptyForSharedMailboxesWhenNoExtension(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(ANDRE.asString, inbox(ANDRE),
AppendCommand.from(
Message.Builder
@@ -714,7 +714,7 @@
def inMailboxOtherThanFilterShouldReturnEmptyForSharedMailboxesWhenNoExtension(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(ANDRE.asString, inbox(ANDRE),
AppendCommand.from(
Message.Builder
@@ -775,8 +775,8 @@
def inMailboxOtherThanFilterShouldAcceptSharedMailboxesWhenExtension(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
val andreInboxId = mailboxProbe.createMailbox(inbox(ANDRE))
- val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe.createMailbox(inbox(BOB))
+ mailboxProbe
.appendMessage(ANDRE.asString, inbox(ANDRE),
AppendCommand.from(
Message.Builder
@@ -851,7 +851,7 @@
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(mailboxPath)
val requestDate = Date.from(ZonedDateTime.now().minusDays(1).toInstant)
val messageId1: MessageId = sendMessageToBobInbox(server, message, requestDate)
- val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, mailboxPath, AppendCommand.from(
ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
.getMessageId
@@ -989,7 +989,7 @@
@Test
def headerExistsShouldBeCaseInsentive(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+ mailboxProbe.createMailbox(inbox(BOB))
val messageId1: MessageId = mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
@@ -1000,7 +1000,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1060,7 +1060,7 @@
@Test
def headerShouldAllowToMatchMailWithSpecificHeaderSet(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+ mailboxProbe.createMailbox(inbox(BOB))
val messageId1: MessageId = mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
@@ -1071,7 +1071,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1131,7 +1131,7 @@
@Test
def headerShouldAllowToMatchMailWithSpecificValueHeaderSet(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+ mailboxProbe.createMailbox(inbox(BOB))
val messageId1: MessageId = mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
@@ -1142,7 +1142,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1151,7 +1151,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId3: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1212,7 +1212,7 @@
@Test
def headerContainsShouldBeCaseInsentive(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
+ mailboxProbe.createMailbox(inbox(BOB))
val messageId1: MessageId = mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
@@ -1223,7 +1223,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1232,7 +1232,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId3: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1293,8 +1293,8 @@
@Test
def headerShouldRejectWhenMoreThanTwoItems(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val bobInboxId = mailboxProbe.createMailbox(inbox(BOB))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe.createMailbox(inbox(BOB))
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1304,7 +1304,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1313,7 +1313,7 @@
.setBody("testmail", StandardCharsets.UTF_8)
.build))
.getMessageId
- val messageId3: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, inbox(BOB),
AppendCommand.from(
Message.Builder
@@ -1377,7 +1377,7 @@
buildTestMessage))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.from(
ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
.getMessageId
@@ -1435,14 +1435,14 @@
val afterRequestDate = Date.from(ZonedDateTime.now().toInstant)
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
- val messageId1: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB),
AppendCommand.builder()
.withInternalDate(beforeRequestDate)
.build(buildTestMessage))
.getMessageId
- val messageId2: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
.withInternalDate(beforeRequestDate)
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
@@ -1455,7 +1455,7 @@
.build(buildTestMessage))
.getMessageId
- val messageId4: MessageId = mailboxProbe
+ mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
.withInternalDate(afterRequestDate)
.build(ClassLoaderUtils.getSystemResourceAsSharedStream("eml/multipart_simple.eml")))
@@ -2902,17 +2902,17 @@
@Test
def minSizeShouldBeInclusive(server: GuiceJamesServer): Unit = {
val message1: Message = simpleMessage("short")
- val size1: Int = computeSize(message1)
+ computeSize(message1)
// One char more than message1
val message2: Message = simpleMessage("short!")
val size2: Int = computeSize(message2)
// One char more than message2
val message3: Message = simpleMessage("short!!")
- val size3: Int = computeSize(message3)
+ computeSize(message3)
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(inbox(BOB))
- val id1 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId
+ mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId
val id2 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId
val id3 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId
@@ -2959,19 +2959,19 @@
@Test
def maxSizeShouldBeExclusive(server: GuiceJamesServer): Unit = {
val message1: Message = simpleMessage("looooooooooooooong")
- val size1: Int = computeSize(message1)
+ computeSize(message1)
// One char more than message3
val message2: Message = simpleMessage("looooooooooooooong!")
val size2: Int = computeSize(message2)
// One char more than message4
val message3: Message = simpleMessage("looooooooooooooong!!")
- val size3: Int = computeSize(message3)
+ computeSize(message3)
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(inbox(BOB))
val id1 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message1)).getMessageId
- val id2 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId
- val id3 = mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId
+ mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message2)).getMessageId
+ mailboxProbe.appendMessage(BOB.asString, inbox(BOB), AppendCommand.from(message3)).getMessageId
val request =
s"""{
@@ -3894,7 +3894,7 @@
.withInternalDate(Date.from(ZonedDateTime.now().minusDays(2).toInstant))
.build(message))
.getMessageId
- val messageId2: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
.withInternalDate(Date.from(ZonedDateTime.now().minusDays(1).toInstant))
.build(message))
@@ -4471,13 +4471,13 @@
.setFrom("user@domain.tld")
.build))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setFrom("other@domain.tld")
.build))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setFrom("yet@other.tld")
@@ -4489,7 +4489,7 @@
.setFrom("yet@other.tld", "user@domain.tld")
.build))
.getMessageId
- val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -4549,7 +4549,7 @@
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setFrom("user@domain.tld")
@@ -4567,7 +4567,7 @@
.setFrom("display@other.tld")
.build))
.getMessageId
- val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -4621,13 +4621,13 @@
.setTo("user@domain.tld")
.build))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setTo("other@domain.tld")
.build))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setTo("yet@other.tld")
@@ -4639,7 +4639,7 @@
.setTo("yet@other.tld", "user@domain.tld")
.build))
.getMessageId
- val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -4699,7 +4699,7 @@
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setTo("user@domain.tld")
@@ -4717,7 +4717,7 @@
.setTo("display@other.tld")
.build))
.getMessageId
- val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -4771,13 +4771,13 @@
.setCc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
.build))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setCc(DefaultAddressParser.DEFAULT.parseMailbox("other@domain.tld"))
.build))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setCc(DefaultAddressParser.DEFAULT.parseMailbox("yet@other.tld"))
@@ -4790,7 +4790,7 @@
DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
.build))
.getMessageId
- val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -4850,7 +4850,7 @@
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setCc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
@@ -4868,7 +4868,7 @@
.setCc(DefaultAddressParser.DEFAULT.parseMailbox("display@other.tld"))
.build))
.getMessageId
- val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -4922,13 +4922,13 @@
.setBcc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
.build))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setBcc(DefaultAddressParser.DEFAULT.parseMailbox("other@domain.tld"))
.build))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setBcc(DefaultAddressParser.DEFAULT.parseMailbox("yet@other.tld"))
@@ -4941,7 +4941,7 @@
DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
.build))
.getMessageId
- val messageId5 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -5001,7 +5001,7 @@
.of
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setBcc(DefaultAddressParser.DEFAULT.parseMailbox("user@domain.tld"))
@@ -5019,7 +5019,7 @@
.setBcc(DefaultAddressParser.DEFAULT.parseMailbox("display@other.tld"))
.build))
.getMessageId
- val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -5072,13 +5072,13 @@
.setSubject("Yet another day in paradise")
.build))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setSubject("Welcome to hell")
.build))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -5131,13 +5131,13 @@
.setSubject("Yet another day in paradise")
.build))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder
.setSubject("Welcome to hell")
.build))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
messageBuilder.build))
.getMessageId
@@ -5642,11 +5642,11 @@
def emailQueryShouldSupportAndOperator(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
.getMessageId
@@ -5714,7 +5714,7 @@
.build(message))
.getMessageId
- val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(message))
.getMessageId
@@ -5761,19 +5761,19 @@
def emailQueryShouldSupportNotOperator(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
.withFlags(new Flags("custom"))
.build(message))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
.withFlags(new Flags("another_custom"))
.build(message))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder()
.withFlags(new FlagsBuilder().add("custom", "another_custom").build())
.build(message))
@@ -5926,15 +5926,15 @@
def inMailboxShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
.getMessageId
@@ -5989,15 +5989,15 @@
def inMailboxOtherThanShouldBeRejectedWhenInOperator(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
.getMessageId
- val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new FlagsBuilder().add("custom", "another_custom").build()).build(message))
.getMessageId
@@ -6096,11 +6096,11 @@
def nestedOperatorsShouldBeSupported(server: GuiceJamesServer): Unit = {
val message: Message = buildTestMessage
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("custom")).build(message))
.getMessageId
- val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().withFlags(new Flags("another_custom")).build(message))
.getMessageId
@@ -6197,7 +6197,7 @@
val message: Message = buildTestMessage
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
- val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.appendMessage(BOB.asString, MailboxPath.inbox(BOB),
AppendCommand.builder()
.withFlags(new FlagsBuilder().add("custom_1", "custom_2").build())
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 413fdd8..f77c7c1 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -1101,7 +1101,7 @@
@Test
def createShouldRejectEmptyMailboxIds(server: GuiceJamesServer): Unit = {
val andrePath = MailboxPath.inbox(ANDRE)
- val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
val request =
s"""{
@@ -1142,7 +1142,7 @@
@Test
def createShouldRejectInvalidMailboxIds(server: GuiceJamesServer): Unit = {
val andrePath = MailboxPath.inbox(ANDRE)
- val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
val request =
s"""{
@@ -1185,7 +1185,7 @@
@Test
def createShouldRejectNoMailboxIds(server: GuiceJamesServer): Unit = {
val andrePath = MailboxPath.inbox(ANDRE)
- val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
val request =
s"""{
@@ -2923,7 +2923,7 @@
val messageId = responseAsJson
.\("id")
.get.asInstanceOf[JsString].value
- val size = responseAsJson
+ responseAsJson
.\("size")
.get.asInstanceOf[JsNumber].value
@@ -3255,7 +3255,7 @@
val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
- val uploadResponse: String = `given`
+ `given`
.basePath("")
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.contentType("text/plain")
@@ -5522,7 +5522,7 @@
@Test
def invalidPatchPropertyShouldFail(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+ mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
val messageId: MessageId = mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB),
@@ -5571,7 +5571,7 @@
@Test
def invalidMailboxPartialUpdatePropertyShouldFail(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+ mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
val messageId: MessageId = mailboxProbe
.appendMessage(BOB.asString, MailboxPath.inbox(BOB),
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index 1c1f40a..dad2ea1 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -1054,7 +1054,7 @@
val provisioningState: State = provisionSystemMailboxes(server)
val path1 = MailboxPath.forUser(BOB, "mailbox1")
- val mailboxId1: String = mailboxProbe
+ mailboxProbe
.createMailbox(path1)
.serialize
@@ -1226,7 +1226,7 @@
.createMailbox(MailboxPath.forUser(BOB, "mailbox5"))
.serialize
- val mailboxId6: String = mailboxProbe
+ mailboxProbe
.createMailbox(MailboxPath.forUser(BOB, "mailbox6"))
.serialize
@@ -1634,7 +1634,7 @@
.setSubject("test")
.setBody("testmail", StandardCharsets.UTF_8)
.build
- val messageId1: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
+ mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val messageId2: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
val messageId3: MessageId = mailboxProbe.appendMessage(BOB.asString(), path, AppendCommand.from(message)).getMessageId
@@ -1977,7 +1977,7 @@
}
private def provisionSystemMailboxes(server: GuiceJamesServer): State = {
- val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
val jmapGuiceProbe: JmapGuiceProbe = server.getProbe(classOf[JmapGuiceProbe])
val request =
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala
index 8cd1898..25ad0b2 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxQueryMethodContract.scala
@@ -184,7 +184,7 @@
@Test
def queryByRoleShouldNotReturnDelegatedMailboxes(server: GuiceJamesServer): Unit = {
val andreInbox = MailboxPath.inbox(ANDRE)
- val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.createMailbox(andreInbox)
val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
.createMailbox(MailboxPath.inbox(BOB))
@@ -245,7 +245,7 @@
@Test
def queryByRoleShouldNotReturnDelegatedMailboxesWhenCaseVariation(server: GuiceJamesServer): Unit = {
val andreInbox = MailboxPath.inbox(ANDRE)
- val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
+ server.getProbe(classOf[MailboxProbeImpl])
.createMailbox(andreInbox)
val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl])
.createMailbox(MailboxPath.forUser(BOB, "InBoX"))
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
index 624c428..ea23e11 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
@@ -7644,7 +7644,7 @@
@Test
def updateShouldHandleNotFoundClientId(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
- val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+ mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
val response = `given`
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala
index 04cfdd8..d31acd0 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/ThreadGetContract.scala
@@ -19,6 +19,8 @@
package org.apache.james.jmap.rfc8621.contract
+import java.nio.charset.StandardCharsets
+
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
import io.restassured.RestAssured.{`given`, requestSpecification}
import io.restassured.http.ContentType.JSON
@@ -28,6 +30,11 @@
import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.MessageManager
+import org.apache.james.mailbox.model.MailboxPath
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.mime4j.stream.RawField
+import org.apache.james.modules.MailboxProbeImpl
import org.apache.james.utils.DataProbeImpl
import org.junit.jupiter.api.{BeforeEach, Test}
@@ -46,7 +53,7 @@
}
@Test
- def threadsShouldReturnSuppliedIds(): Unit = {
+ def givenNonMessageThenGetThreadsShouldReturnNotFound(): Unit = {
val request =
s"""{
| "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
@@ -62,47 +69,6 @@
val response = `given`
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(request)
- .when
- .post
- .`then`
- .statusCode(SC_OK)
- .contentType(JSON)
- .extract
- .body
- .asString
-
- assertThatJson(response)
- .inPath("methodResponses[0][1]")
- .isEqualTo(
- s"""{
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${SESSION_STATE.value}",
- | "list": [
- | {
- | "id": "123456",
- | "emailIds": ["123456"]
- | }
- | ]
- |}""".stripMargin)
- }
-
- @Test
- def threadsShouldReturnSuppliedIdsWhenSeveralThreads(): Unit = {
- val request =
- s"""{
- | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
- | "methodCalls": [[
- | "Thread/get",
- | {
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "ids": ["123456", "789"]
- | },
- | "c1"]]
- |}""".stripMargin
-
- val response = `given`
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(request)
.when
.post
.`then`
@@ -113,22 +79,26 @@
.asString
assertThatJson(response)
- .inPath("methodResponses[0][1]")
.isEqualTo(
s"""{
- | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
- | "state": "${SESSION_STATE.value}",
- | "list": [
- | {
- | "id": "123456",
- | "emailIds": ["123456"]
- | },
- | {
- | "id": "789",
- | "emailIds": ["789"]
- | }
- | ]
- |}""".stripMargin)
+ | "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "methodResponses": [
+ | [
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [
+ |
+ | ],
+ | "notFound": [
+ | "123456"
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
}
@Test
@@ -172,4 +142,409 @@
| ]
|}""".stripMargin)
}
+
+ @Test
+ def addRelatedMailsInAThreadThenGetThatThreadShouldReturnExactThreadObjectWithEmailIdsSortedByArrivalDate(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ // given 3 mails with related Subject and related Mime Message-ID fields
+ val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+ .setMessageId("Message-ID-1")
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ // message2 reply to message1
+ val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+ .setMessageId("Message-ID-2")
+ .setField(new RawField("In-Reply-To", "Message-ID-1"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ // message3 related to message1 through Subject and References message1's Message-ID
+ val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Fwd: Re: Test")
+ .setMessageId("Message-ID-3")
+ .setField(new RawField("In-Reply-To", "Random-InReplyTo"))
+ .addField(new RawField("References", "Message-ID-1"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ val threadId = message1.getThreadId.serialize()
+ val message1Id = message1.getId.getMessageId.serialize()
+ val message2Id = message2.getId.getMessageId.serialize()
+ val message3Id = message3.getId.getMessageId.serialize()
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["$threadId"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "methodResponses": [
+ | [
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [{
+ | "id": "$threadId",
+ | "emailIds": ["$message1Id", "$message2Id", "$message3Id"]
+ | }],
+ | "notFound": [
+ |
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def givenTwoThreadGetThatTwoThreadShouldReturnExactTwoThreadObjectWithEmailIdsSortedByArrivalDate(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ // given 2 mails with related Subject and related Mime Message-ID fields in threadA
+ val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+ .setMessageId("Message-ID-1")
+ .setBody("testmail", StandardCharsets.UTF_8)))
+ // message2 reply to message1
+ val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+ .setMessageId("Message-ID-2")
+ .setField(new RawField("In-Reply-To", "Message-ID-1"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+ val threadA = message1.getThreadId.serialize()
+
+ // message3 in threadB
+ val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Message3-SubjectLine")
+ .setMessageId("Message-ID-3")
+ .setBody("testmail", StandardCharsets.UTF_8)))
+ val threadB = message3.getThreadId.serialize()
+
+ val message1Id = message1.getId.getMessageId.serialize()
+ val message2Id = message2.getId.getMessageId.serialize()
+ val message3Id = message3.getId.getMessageId.serialize()
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["$threadA", "$threadB"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "methodResponses": [
+ | [
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [{
+ | "id": "$threadA",
+ | "emailIds": [
+ | "$message1Id",
+ | "$message2Id"
+ | ]
+ | },
+ | {
+ | "id": "$threadB",
+ | "emailIds": [
+ | "$message3Id"
+ | ]
+ | }
+ | ],
+ | "notFound": [
+ |
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def givenOneThreadGetTwoThreadShouldReturnOnlyOneThreadObjectAndNotFound(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ // given 2 mails with related Subject and related Mime Message-ID fields in threadA
+ val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+ .setMessageId("Message-ID-1")
+ .setBody("testmail", StandardCharsets.UTF_8)))
+ // message2 reply to message1
+ val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+ .setMessageId("Message-ID-2")
+ .setField(new RawField("In-Reply-To", "Message-ID-1"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+ val threadA = message1.getThreadId.serialize()
+
+ val message1Id = message1.getId.getMessageId.serialize()
+ val message2Id = message2.getId.getMessageId.serialize()
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["$threadA", "nonExistThread"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "methodResponses": [
+ | [
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [{
+ | "id": "$threadA",
+ | "emailIds": [
+ | "$message1Id",
+ | "$message2Id"
+ | ]
+ | }],
+ | "notFound": [
+ | "nonExistThread"
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def addThreeMailsWithRelatedSubjectButNonIdenticalMimeMessageIDThenGetThatThreadShouldNotReturnUnrelatedMails(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test")
+ .setMessageId("Message-ID-1")
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ // message2 have related subject with message1 but non identical Mime Message-ID
+ val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Re: Test")
+ .setMessageId("Message-ID-2")
+ .setField(new RawField("In-Reply-To", "Random-InReplyTo"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ // message3 have related subject with message1 but non identical Mime Message-ID
+ val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Fwd: Re: Test")
+ .setMessageId("Message-ID-3")
+ .setField(new RawField("In-Reply-To", "Another-Random-InReplyTo"))
+ .addField(new RawField("References", "Random-References"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ val threadId1 = message1.getThreadId.serialize()
+ val message1Id = message1.getId.getMessageId.serialize()
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["$threadId1"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "methodResponses": [
+ | [
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [{
+ | "id": "$threadId1",
+ | "emailIds": ["$message1Id"]
+ | }],
+ | "notFound": [
+ |
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def addThreeMailsWithIdenticalMimeMessageIDButNonRelatedSubjectThenGetThatThreadShouldNotReturnUnrelatedMails(server: GuiceJamesServer): Unit = {
+ val bobPath = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+ val message1: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test1")
+ .setMessageId("Message-ID-1")
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ // message2 have identical Mime Message-ID with message1 through In-Reply-To field but have non related subject
+ val message2: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test2")
+ .setMessageId("Message-ID-2")
+ .setField(new RawField("In-Reply-To", "Message-ID-1"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ // message2 have identical Mime Message-ID with message1 through References field but have non related subject
+ val message3: MessageManager.AppendResult = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessageAndGetAppendResult(BOB.asString(), bobPath,
+ MessageManager.AppendCommand.from(Message.Builder.of.setSubject("Test3")
+ .setMessageId("Message-ID-3")
+ .setField(new RawField("In-Reply-To", "Random-InReplyTo"))
+ .addField(new RawField("References", "Message-ID-1"))
+ .setBody("testmail", StandardCharsets.UTF_8)))
+
+ val threadId1 = message1.getThreadId.serialize()
+ val message1Id = message1.getId.getMessageId.serialize()
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["$threadId1"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "methodResponses": [
+ | [
+ | "Thread/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+ | "list": [{
+ | "id": "$threadId1",
+ | "emailIds": ["$message1Id"]
+ | }],
+ | "notFound": [
+ |
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index 0223d8a..45ae356 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -1069,7 +1069,7 @@
def pushEnableRequestWithPushStateShouldReturnServerState(server: GuiceJamesServer): Unit = {
val bobPath = MailboxPath.inbox(BOB)
val accountId: AccountId = AccountId.fromUsername(BOB)
- val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
Thread.sleep(100)
diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index 526270c..42d6fb0 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -189,6 +189,13 @@
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>io.github.evis</groupId>
+ <artifactId>scalafix-maven-plugin</artifactId>
+ <configuration>
+ <config>${project.parent.parent.basedir}/.scalafix.conf</config>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala
index 140ecfb..37cdd28 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/UserProvisioning.scala
@@ -25,7 +25,6 @@
import org.apache.james.core.Username
import org.apache.james.mailbox.MailboxSession
import org.apache.james.metrics.api.MetricFactory
-import org.apache.james.metrics.api.TimeMetric.ExecutionResult.DEFAULT_100_MS_THRESHOLD
import org.apache.james.user.api.{AlreadyExistInUsersRepositoryException, UsersRepository, UsersRepositoryException}
import reactor.core.scala.publisher.SMono
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index 2223077..43167af 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -21,15 +21,14 @@
import eu.timepit.refined
import org.apache.james.jmap.api.model.Preview
+import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.Id.IdConstraint
import org.apache.james.jmap.core.{Properties, UuidState}
-import org.apache.james.jmap.mail.Email.Size
import org.apache.james.jmap.mail.{AddressesHeaderValue, BlobId, Charset, DateHeaderValue, Disposition, EmailAddress, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailChangesRequest, EmailChangesResponse, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, EmailerName, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextBodyValues, GroupName, GroupedAddressesHeaderValue, HasAttachment, HeaderMessageId, HeaderURL, IsEncodingProblem, IsTruncated, Keyword, Keywords, Language, Languages, Location, MailboxIds, MessageIdsHeaderValue, Name, PartId, RawHeaderValue, Subject, TextHeaderValue, ThreadId, Type, URLsHeaderValue, UnparsedEmailId}
import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
import play.api.libs.functional.syntax._
import play.api.libs.json._
-import scala.language.implicitConversions
object EmailBodyPartToSerialize {
def from(part: EmailBodyPart): EmailBodyPartToSerialize = EmailBodyPartToSerialize(
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
index 11ff719..c41e255 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailQuerySerializer.scala
@@ -25,7 +25,6 @@
import org.apache.james.mailbox.model.{MailboxId, MessageId}
import play.api.libs.json._
-import scala.language.implicitConversions
import scala.util.Try
class EmailQuerySerializer @Inject()(mailboxIdFactory: MailboxId.Factory) {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
index 9f4d74b..ca5da3e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/ThreadSerializer.scala
@@ -20,16 +20,21 @@
package org.apache.james.jmap.json
import org.apache.james.jmap.core.UuidState
-import org.apache.james.jmap.mail.{Thread, ThreadChangesRequest, ThreadChangesResponse, ThreadGetRequest, ThreadGetResponse}
-import play.api.libs.json.{JsObject, JsResult, JsValue, Json, OWrites, Reads, Writes}
+import org.apache.james.jmap.mail.{Thread, ThreadChangesRequest, ThreadChangesResponse, ThreadGetRequest, ThreadGetResponse, ThreadNotFound, UnparsedThreadId}
+import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.{JsObject, JsResult, JsString, JsValue, Json, OWrites, Reads, Writes}
import scala.language.implicitConversions
object ThreadSerializer {
+ private implicit val messageIdWrites: Writes[MessageId] = messageId => JsString(messageId.serialize())
+ private implicit val unparsedThreadIdReads: Reads[UnparsedThreadId] = Json.valueReads[UnparsedThreadId]
private implicit val threadGetReads: Reads[ThreadGetRequest] = Json.reads[ThreadGetRequest]
private implicit val threadChangesReads: Reads[ThreadChangesRequest] = Json.reads[ThreadChangesRequest]
private implicit val threadWrites: OWrites[Thread] = Json.writes[Thread]
private implicit val stateWrites: Writes[UuidState] = Json.valueWrites[UuidState]
+ private implicit val unparsedThreadIdWrites: Writes[UnparsedThreadId] = Json.valueWrites[UnparsedThreadId]
+ private implicit val threadNotFoundWrites: Writes[ThreadNotFound] = Json.valueWrites[ThreadNotFound]
private implicit val threadGetWrites: OWrites[ThreadGetResponse] = Json.writes[ThreadGetResponse]
private implicit val changesResponseWrites: OWrites[ThreadChangesResponse] = Json.writes[ThreadChangesResponse]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index db66bfa..e77c41f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -21,17 +21,14 @@
import cats.implicits._
import eu.timepit.refined
-import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
-import eu.timepit.refined.numeric.NonNegative
-import eu.timepit.refined.refineV
import eu.timepit.refined.types.string.NonEmptyString
import org.apache.james.jmap.api.model.Preview
+import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
import org.apache.james.jmap.api.projections.{MessageFastViewPrecomputedProperties, MessageFastViewProjection}
import org.apache.james.jmap.core.Id.{Id, IdConstraint}
import org.apache.james.jmap.core.{Properties, UTCDate}
import org.apache.james.jmap.mail.BracketHeader.sanitize
-import org.apache.james.jmap.mail.Email.{Size, sanitizeSize}
import org.apache.james.jmap.mail.EmailHeaderName.{ADDRESSES_NAMES, DATE, MESSAGE_ID_NAMES}
import org.apache.james.jmap.mail.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
import org.apache.james.jmap.method.ZoneIdProvider
@@ -76,18 +73,6 @@
case scala.Right(value) => Success(UnparsedEmailId(value))
}
- type Size = Long Refined NonNegative
- val Zero: Size = 0L
-
- def sanitizeSize(value: Long): Size = {
- val size: Either[String, Size] = refineV[NonNegative](value)
- size.fold(e => {
- logger.error(s"Encountered an invalid Email size: $e")
- Zero
- },
- refinedValue => refinedValue)
- }
-
private[mail] def parseAsMime4JMessage(firstMessage: MessageResult): Try[Message] = {
val defaultMessageBuilder = new DefaultMessageBuilder
defaultMessageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index e3d08a8..09c2bb9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -20,7 +20,6 @@
package org.apache.james.jmap.mail
import java.io.OutputStream
-
import cats.implicits._
import com.google.common.io.CountingOutputStream
import eu.timepit.refined.api.Refined
@@ -28,8 +27,8 @@
import eu.timepit.refined.numeric.NonNegative
import eu.timepit.refined.refineV
import org.apache.commons.io.IOUtils
+import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.Properties
-import org.apache.james.jmap.mail.Email.Size
import org.apache.james.jmap.mail.EmailBodyPart.{FILENAME_PREFIX, MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN}
import org.apache.james.jmap.mail.PartId.PartIdValue
import org.apache.james.mailbox.model.{Cid, MessageId, MessageResult}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
index 1a7a676..de06a1e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailQuery.scala
@@ -20,10 +20,10 @@
package org.apache.james.jmap.mail
import cats.implicits._
+import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.Limit.Limit
import org.apache.james.jmap.core.Position.Position
import org.apache.james.jmap.core.{AccountId, CanCalculateChanges, LimitUnparsed, PositionUnparsed, QueryState, UTCDate}
-import org.apache.james.jmap.mail.Email.Size
import org.apache.james.jmap.mail.IsAscending.ASCENDING
import org.apache.james.jmap.method.WithAccountId
import org.apache.james.mailbox.model.SearchQuery.Sort.Order.{NATURAL, REVERSE}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index ef69a2c..afa1b41 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -20,15 +20,14 @@
import java.nio.charset.{StandardCharsets, Charset => NioCharset}
import java.util.Date
-
import cats.implicits._
import com.google.common.net.MediaType
import com.google.common.net.MediaType.{HTML_UTF_8, PLAIN_TEXT_UTF_8}
import eu.timepit.refined
+import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.Id.{Id, IdConstraint}
import org.apache.james.jmap.core.{AccountId, SetError, UTCDate, UuidState}
import org.apache.james.jmap.mail.Disposition.INLINE
-import org.apache.james.jmap.mail.Email.Size
import org.apache.james.jmap.method.WithAccountId
import org.apache.james.jmap.routes.{Blob, BlobResolvers}
import org.apache.james.mailbox.MailboxSession
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
index e39c20d..e6619a5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Thread.scala
@@ -23,15 +23,23 @@
import org.apache.james.jmap.core.UnsignedInt.UnsignedInt
import org.apache.james.jmap.core.{AccountId, UuidState}
import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.mailbox.model.MessageId
-case class Thread(id: Id, emailIds: List[Id])
+case class Thread(id: Id, emailIds: List[MessageId])
case class ThreadGetRequest(accountId: AccountId,
- ids: List[Id]) extends WithAccountId
+ ids: List[UnparsedThreadId]) extends WithAccountId
case class ThreadGetResponse(accountId: AccountId,
state: UuidState,
- list: List[Thread])
+ list: List[Thread],
+ notFound: ThreadNotFound)
+
+case class ThreadNotFound(value: Set[UnparsedThreadId]) {
+ def merge(other: ThreadNotFound): ThreadNotFound = ThreadNotFound(this.value ++ other.value)
+}
+
+case class UnparsedThreadId(id: Id)
case class ThreadChangesRequest(accountId: AccountId,
sinceState: UuidState,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala
index 5598083..f0eb461 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailImportMethod.scala
@@ -20,17 +20,18 @@
package org.apache.james.jmap.method
import java.util.Date
-
import eu.timepit.refined.auto._
+
import javax.inject.Inject
import org.apache.james.jmap.api.change.EmailChangeRepository
+import org.apache.james.jmap.api.model.Size.sanitizeSize
import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JAMES_SHARES, JMAP_CORE, JMAP_MAIL}
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.core.SetError.SetErrorDescription
import org.apache.james.jmap.core.{ClientId, Id, Invocation, ServerId, SetError, UuidState}
import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
-import org.apache.james.jmap.mail.{BlobId, Email, EmailCreationId, EmailCreationResponse, EmailImport, EmailImportRequest, EmailImportResponse, ThreadId, ValidatedEmailImport}
+import org.apache.james.jmap.mail.{BlobId, EmailCreationId, EmailCreationResponse, EmailImport, EmailImportRequest, EmailImportResponse, ThreadId, ValidatedEmailImport}
import org.apache.james.jmap.method.EmailImportMethod.{ImportFailure, ImportResult, ImportResults, ImportSuccess, ImportWithBlob}
import org.apache.james.jmap.routes.{Blob, BlobNotFoundException, BlobResolvers, ProcessingContext, SessionSupplier}
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -173,7 +174,7 @@
private def asEmailCreationResponse(appendResult: MessageManager.AppendResult): EmailCreationResponse = {
val blobId: Option[BlobId] = BlobId.of(appendResult.getId.getMessageId).toOption
val threadId: ThreadId = ThreadId.fromJava(appendResult.getThreadId)
- EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, Email.sanitizeSize(appendResult.getSize))
+ EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, sanitizeSize(appendResult.getSize))
}
private def retrieveState(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession): SMono[UuidState] =
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
index 11d2e06..bdb401f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
@@ -21,15 +21,16 @@
import java.time.ZonedDateTime
import java.util.Date
-
import eu.timepit.refined.auto._
+
import javax.inject.Inject
import javax.mail.Flags
import org.apache.james.jmap.JMAPConfiguration
+import org.apache.james.jmap.api.model.Size.sanitizeSize
import org.apache.james.jmap.core.SetError.SetErrorDescription
import org.apache.james.jmap.core.{Properties, SetError, UTCDate}
import org.apache.james.jmap.json.EmailSetSerializer
-import org.apache.james.jmap.mail.{BlobId, Email, EmailCreationId, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, ThreadId}
+import org.apache.james.jmap.mail.{BlobId, EmailCreationId, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, ThreadId}
import org.apache.james.jmap.method.EmailSetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
import org.apache.james.jmap.routes.{BlobNotFoundException, BlobResolvers}
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -112,7 +113,7 @@
} yield {
val blobId: Option[BlobId] = BlobId.of(appendResult.getId.getMessageId).toOption
val threadId: ThreadId = ThreadId.fromJava(appendResult.getThreadId)
- CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, Email.sanitizeSize(appendResult.getSize)))
+ CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId, blobId, threadId, sanitizeSize(appendResult.getSize)))
}
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala
index 1c3c53e..7b91276 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ThreadGetMethod.scala
@@ -22,31 +22,59 @@
import eu.timepit.refined.auto._
import javax.inject.Inject
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
-import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.core.{Invocation, UuidState}
+import org.apache.james.jmap.core.{AccountId, Invocation, UuidState}
import org.apache.james.jmap.json.{ResponseSerializer, ThreadSerializer}
-import org.apache.james.jmap.mail.{Thread, ThreadGetRequest, ThreadGetResponse}
+import org.apache.james.jmap.mail.{Thread, ThreadGetRequest, ThreadGetResponse, ThreadNotFound, UnparsedThreadId}
import org.apache.james.jmap.routes.SessionSupplier
-import org.apache.james.mailbox.MailboxSession
+import org.apache.james.mailbox.model.{ThreadId => JavaThreadId}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
import org.apache.james.metrics.api.MetricFactory
import play.api.libs.json.{JsError, JsSuccess}
-import reactor.core.scala.publisher.SMono
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.util.Try
+
+object ThreadGetResult {
+ def empty: ThreadGetResult = ThreadGetResult(Set.empty, ThreadNotFound(Set.empty))
+
+ def merge(result1: ThreadGetResult, result2: ThreadGetResult): ThreadGetResult = result1.merge(result2)
+
+ def found(thread: Thread): ThreadGetResult =
+ ThreadGetResult(Set(thread), ThreadNotFound(Set.empty))
+
+ def notFound(unparsedThreadId: UnparsedThreadId): ThreadGetResult =
+ ThreadGetResult(Set.empty, ThreadNotFound(Set(unparsedThreadId)))
+}
+
+case class ThreadGetResult(threads: Set[Thread], notFound: ThreadNotFound) {
+ def merge(other: ThreadGetResult): ThreadGetResult =
+ ThreadGetResult(this.threads ++ other.threads, this.notFound.merge(other.notFound))
+
+ def asResponse(accountId: AccountId): ThreadGetResponse =
+ ThreadGetResponse(
+ accountId = accountId,
+ state = UuidState.INSTANCE,
+ list = threads.toList,
+ notFound = notFound)
+}
class ThreadGetMethod @Inject()(val metricFactory: MetricFactory,
- val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[ThreadGetRequest] {
+ val sessionSupplier: SessionSupplier,
+ val threadIdFactory: JavaThreadId.Factory,
+ val mailboxManager: MailboxManager) extends MethodRequiringAccountId[ThreadGetRequest] {
override val methodName: MethodName = MethodName("Thread/get")
override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: ThreadGetRequest): SMono[InvocationWithContext] = {
- val response = ThreadGetResponse(accountId = request.accountId,
- state = UuidState.INSTANCE,
- list = retrieveThreads(request.ids))
- SMono.just(InvocationWithContext(invocation = Invocation(
- methodName = methodName,
- arguments = Arguments(ThreadSerializer.serialize(response)),
- methodCallId = invocation.invocation.methodCallId),
- processingContext = invocation.processingContext))
+ getThreadResponse(request, mailboxSession)
+ .reduce(ThreadGetResult.empty)(ThreadGetResult.merge)
+ .map(threadGetResult => threadGetResult.asResponse(request.accountId))
+ .map(threadGetResponse => Invocation(
+ methodName = methodName,
+ arguments = Arguments(ThreadSerializer.serialize(threadGetResponse)),
+ methodCallId = invocation.invocation.methodCallId))
+ .map(InvocationWithContext(_, invocation.processingContext))
}
override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, ThreadGetRequest] =
@@ -55,7 +83,18 @@
case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
}
- // Naive implementation
- private def retrieveThreads(ids: List[Id]): List[Thread] =
- ids.map(id => Thread(id = id, emailIds = List(id)))
+ private def getThreadResponse(threadGetRequest: ThreadGetRequest,
+ mailboxSession: MailboxSession): SFlux[ThreadGetResult] = {
+ SFlux.fromIterable(threadGetRequest.ids)
+ .flatMap(unparsedThreadId => {
+ Try(threadIdFactory.fromString(unparsedThreadId.id.toString()))
+ .fold(e => SFlux.just(ThreadGetResult.notFound(unparsedThreadId)),
+ threadId => SFlux.fromPublisher(mailboxManager.getThread(threadId, mailboxSession))
+ .collectSeq()
+ .map(seq => Thread(id = unparsedThreadId.id, emailIds = seq.toList))
+ .map(ThreadGetResult.found)
+ .onErrorResume((_ => SMono.just(ThreadGetResult.notFound(unparsedThreadId)))))
+ })
+ }
+
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
index d1a90db..614b28f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
@@ -22,7 +22,6 @@
import java.nio.charset.StandardCharsets
import java.util.stream
import java.util.stream.Stream
-
import com.google.common.base.CharMatcher
import eu.timepit.refined.numeric.NonNegative
import eu.timepit.refined.refineV
@@ -30,15 +29,16 @@
import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE}
import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, NOT_FOUND, OK, UNAUTHORIZED}
import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus, QueryStringDecoder}
+
import javax.inject.{Inject, Named}
import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
+import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.{AccountId, Id, ProblemDetails}
import org.apache.james.jmap.exceptions.UnauthorizedException
import org.apache.james.jmap.http.Authenticator
import org.apache.james.jmap.http.rfc8621.InjectionKeys
import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.mail.Email.Size
import org.apache.james.jmap.mail.{BlobId, EmailBodyPart, PartId}
import org.apache.james.jmap.routes.DownloadRoutes.{BUFFER_SIZE, LOGGER}
import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
@@ -101,7 +101,7 @@
}
case class AttachmentBlob(attachmentMetadata: AttachmentMetadata, fileContent: InputStream) extends Blob {
- override def size: Try[Size] = Success(UploadRoutes.sanitizeSize(attachmentMetadata.getSize))
+ override def size: Try[Size] = Success(sanitizeSize(attachmentMetadata.getSize))
override def contentType: ContentType = attachmentMetadata.getType
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
index 418e5ab..ff30311 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -19,22 +19,12 @@
package org.apache.james.jmap.routes
-import java.io.InputStream
-import java.nio.ByteBuffer
-import java.nio.charset.StandardCharsets
-import java.util.stream
-import java.util.stream.Stream
-
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.auto._
-import eu.timepit.refined.numeric.NonNegative
-import eu.timepit.refined.refineV
import io.netty.handler.codec.http.HttpHeaderNames.{CONTENT_LENGTH, CONTENT_TYPE}
import io.netty.handler.codec.http.HttpResponseStatus.{BAD_REQUEST, CREATED, FORBIDDEN, INTERNAL_SERVER_ERROR, UNAUTHORIZED}
import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
-import javax.inject.{Inject, Named}
import org.apache.commons.fileupload.util.LimitedInputStream
import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
+import org.apache.james.jmap.api.model.Size.{Size, sanitizeSize}
import org.apache.james.jmap.core.Id.Id
import org.apache.james.jmap.core.{AccountId, Id, JmapRfc8621Configuration, ProblemDetails}
import org.apache.james.jmap.exceptions.UnauthorizedException
@@ -42,8 +32,7 @@
import org.apache.james.jmap.http.rfc8621.InjectionKeys
import org.apache.james.jmap.json.{ResponseSerializer, UploadSerializer}
import org.apache.james.jmap.mail.BlobId
-import org.apache.james.jmap.mail.Email.Size
-import org.apache.james.jmap.routes.UploadRoutes.{LOGGER, sanitizeSize}
+import org.apache.james.jmap.routes.UploadRoutes.LOGGER
import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
import org.apache.james.mailbox.model.{AttachmentMetadata, ContentType}
import org.apache.james.mailbox.{AttachmentManager, MailboxSession}
@@ -55,22 +44,17 @@
import reactor.core.scheduler.Schedulers
import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
+import java.io.InputStream
+import java.nio.ByteBuffer
+import java.nio.charset.StandardCharsets
+import java.util.stream
+import java.util.stream.Stream
+import javax.inject.{Inject, Named}
+
case class TooBigUploadException() extends RuntimeException
object UploadRoutes {
val LOGGER: Logger = LoggerFactory.getLogger(classOf[DownloadRoutes])
-
- type Size = Long Refined NonNegative
- val Zero: Size = 0L
-
- def sanitizeSize(value: Long): Size = {
- val size: Either[String, Size] = refineV[NonNegative](value)
- size.fold(e => {
- LOGGER.error(s"Encountered an invalid upload files size: $e")
- Zero
- },
- refinedValue => refinedValue)
- }
}
case class UploadResponse(accountId: AccountId,
diff --git a/src/adr/0048-cleanup-jmap-uploads.md b/src/adr/0048-cleanup-jmap-uploads.md
new file mode 100644
index 0000000..62ad15d
--- /dev/null
+++ b/src/adr/0048-cleanup-jmap-uploads.md
@@ -0,0 +1,87 @@
+# 48. Cleanup of JMAP uploads
+
+Date: 2021-07-21
+
+## Status
+
+Accepted (lazy consensus).
+
+Not yet implemented.
+
+## Context
+
+JMAP allows users to upload binary content called blobs to be later referenced via method calls. This includes but is not
+limited to `Email/set` for specifying the blobId of attachments and `Email/import`.
+
+The [specification](https://jmap.io/spec-core.html#binary-data) strongly encourages enforcing the cleanup of these uploads:
+
+```
+A blob that is not referenced by a JMAP object (e.g., as a message attachment) MAY be deleted by the server to free up
+resources. Uploads (see below) are initially unreferenced blobs.
+
+[...] An unreferenced blob MUST NOT be deleted for at least 1 hour from the time of upload; if reuploaded, the same
+blobId MAY be returned, but this SHOULD reset the expiry time.
+```
+
+Deleting such uploads in a timely manner is important as:
+
+ - It enables freeing server resources.
+ - failing to do so may compromise privacy: content the user have uploaded and long forgotten might still be accessible
+ in the underlying data-store. Failing to delete uploads in a timely fashion may jeopardize for instance GDPR compliance.
+
+Today, uploads are stored along side email attachments. This means:
+ - We can hardly apply a specific lifecycle that cleans up uploads, as distinguishing attachment from uploads is not
+ trivial.
+ - We currently have a complex right resolution system on attachment, handling both the upload case (were the attachment
+ is linked to a user) and the 'true' attachment case (linked to a message, those who can access the message can access
+ the attachment). This leads to sub-optimal code (slow).
+
+## Decision
+
+We need to create a separate interface `UploadRepository` in `data-jmap` to store uploads for each user. We would provide a memory
+implementation as well as a distributed implementation of it.
+
+The distributed implementation would host metadata of the upload in Cassandra, and the content using the BlobStore API,
+so object storage.
+
+This `UploadRepository` would be used by JMAP RFC-8620 to back uploads (instead of the attachment manager), we will
+provide a `BlobResolver` to enable interactions with the uploaded blob. Similarly, we will use the `UploadRepository` to
+back uploads of JMAP draft.
+
+We will implement cleanup of the distributed `UploadRepository`. This will be done via:
+ - TTLs on the Cassandra metadata.
+ - Organisation of the blobs in time ranged buckets, only the two most recent buckets are kept.
+ - A WebAdmin endpoint would allow to plan a CRON triggering the cleanup.
+
+## Consequences
+
+Upon migrating to the `UploadRepository`, previous uploads will not be carried over. No migration plan is provided as
+the impact is minimal. Upload prior this change will never be cleaned up. This is acceptable as JMAP implementations are
+marked as experimental.
+
+We can clean up attachment storage within the `mailbox-api` and its implementation:
+ - Drop `attachmentOwners` cassandra table
+ - Remove `getOwners` `storeAttachmentForOwner` methods in the Attachment mapper
+ - Rename `storeAttachmentsForMessage*` -> `storeAttachments*` in attachment mapper
+ - Simplify resolution logic for `StoreAttachmentManager` (looking message ownership is then enough)
+ - Fusion of `attachmentMessageId` and `attachmentV2` table, `attachmentMessageId` to be dropped in next release,
+ `attachmentV2` can be altered to add the referencing `messageId`, and a migration task will be provided to populate it.
+ In the meantime a fallback strategy can be supplied: If the messageId cell is null we should default to reading the
+ (old) `attachmentMessageId` table.
+
+## Alternatives
+
+[JMAP blob draft](https://datatracker.ietf.org/doc/draft-ietf-jmap-blob/) had been proposed to have the clients explicitly
+delete its uploads once the blob had been used to create other entities, as this extension introduce a mean to delete
+blobs.
+
+However, relying on clients to enforce effective deletion seems brittle as:
+ - In case of client failures (or malicious client), no mechanisms would ensure effective deletion
+ - The main JMAP specification does not mandate nor encourage clients to clean up their uploads using the blob extension
+ and as such interoperability issues would arise.
+
+## References
+
+ - [JIRA](https://issues.apache.org/jira/browse/JAMES-3544)
+ - [PR of this ADR](https://github.com/apache/james-project/pull/544)
+ - [Thread on server-dev mailing list](https://www.mail-archive.com/server-dev@james.apache.org/msg70591.html)
\ No newline at end of file
diff --git a/src/homepage/index.html b/src/homepage/index.html
index 85683fc..016e72d 100644
--- a/src/homepage/index.html
+++ b/src/homepage/index.html
@@ -51,9 +51,8 @@
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#posts">Last Posts</a></li>
<li><a href="#second">Community</a></li>
- <li><a href="#third">Contribute</a></li>
- <li><a href="#use-cases">Use-cases</a></li>
<li><a href="documentation.html"><span class="fa fa-external-link"></span> Documentation</a></li>
+ <li><a href="download.cgi"><span class="fa fa-external-link"></span> Downloads</a></li>
</ul>
</nav>
@@ -101,9 +100,23 @@
<section id="first" class="main">
<header class="major">
<h2>Get Started With James</h2>
+ <h3>Get Started With the official binaries </h3>
</header>
<section>
<ul class="james-ul no-padding">
+ <li class="post-template"><span class="long-arrow-right">→</span><span><b>DOWNLOADS:</b><br>
+ The Apache James project wires together the different libraries composing James to provide a running services,
+ ready to download on the Apache mirrors. See
+ <a href="download.cgi">our download page</a>.<br/><br/>
+ Follow <a href="install.html">our installation guide</a>.
+ </span></li>
+ </ul>
+ </section>
+ <section id="first_docker" class="main">
+ <header class="major">
+ <h3>Get Started With James And Docker</h3>
+ </header>
+ <ul class="james-ul no-padding">
<li class="post-template"><span class="long-arrow-right">→</span><span><b>
WHAT WILL YOU TRY:</b><br>
<span>Here you will try James server v 3.6.0 thanks to an image. This James image has a default configuration
diff --git a/src/site/xdoc/download.xml b/src/site/xdoc/download.xml
index 93607c2..e9a1f9d 100644
--- a/src/site/xdoc/download.xml
+++ b/src/site/xdoc/download.xml
@@ -164,7 +164,7 @@
[<a href="https://downloads.apache.org/james/server/3.6.0/james-project-3.6.0-source-release.zip.asc">PGP</a>]</li>
<li>Binary (ZIP Format) for Spring wiring:
- <a href="https://www.apache.org/dyn/closer.lua/james/server/3.6.0/james-server-app-3.6.0-app.zip">apache-james-3.6.0-app.zip</a>
+ <a href="https://www.apache.org/dyn/closer.lua/james/server/3.6.0/james-server-app-3.6.0-app.zip">james-server-app-3.6.0-app.zip</a>
[<a href="https://downloads.apache.org/james/server/3.6.0/james-server-app-3.6.0-app.zip.sha512">SHA-512</a>]
[<a href="https://downloads.apache.org/james/server/3.6.0/james-server-app-3.6.0-app.zip.asc">PGP</a>]
</li>
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index f32ffb4..97bed20 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -15,8 +15,33 @@
Changes to apply between 3.6.x and 3.7.x will be reported here.
Change list:
-
+ - [Adding the threadId to the ElasticSearch index](#adding-the-threadid-to-the-elasticsearch-index)
- [Rework message denormalization](#rework-message-denormalization)
+ - [Adding threadId column to message metadata tables](#adding-threadid-column-to-message-metadata-tables)
+
+### Adding the threadId to the ElasticSearch index
+
+Date 22/07/2021
+
+JIRA: https://issues.apache.org/jira/browse/JAMES-3516
+
+Concerned product: Distributed James
+
+Add threadId to James document mapping to enable thread query search.
+
+We already have this field as part of newly created mappings, but we need to explicitly add this field to existing indices by doing:
+```
+curl -X PUT \
+ http://ip:ESport/mailbox_v1/_mapping \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "properties": {
+ "threadId": {
+ "type": "keyword"
+ }
+ }
+}'
+```
### Rework message denormalization