blob: c54ee332666d2ddab89e5598d13c34b6071ccc22 [file] [log] [blame]
/******************************************************************
* 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.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;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
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() {
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);
}
private static Stream<Arguments> givenOldMailWhenAddNewRelatedMailsThenGuessingThreadIdShouldReturnSameThreadIdWithOldMail() {
return Stream.of(
// mails related to old message by subject and Message-ID (but this should not happen in real world cause every mail should have an unique MimeMessageId)
Arguments.of(Optional.of(new MimeMessageId("Message-ID")), Optional.empty(), Optional.empty(), Optional.of(new Subject("Re: Test"))),
Arguments.of(Optional.of(new MimeMessageId("someInReplyTo")), Optional.empty(), Optional.empty(), Optional.of(new Subject("Re: Test"))),
Arguments.of(Optional.of(new MimeMessageId("references1")), Optional.empty(), Optional.empty(), Optional.of(new Subject("Re: Test"))),
// mails related to old message by subject and In-Reply-To
Arguments.of(Optional.empty(), Optional.of(new MimeMessageId("Message-ID")), Optional.empty(), Optional.of(new Subject("Re: Test"))),
Arguments.of(Optional.empty(), Optional.of(new MimeMessageId("someInReplyTo")), Optional.empty(), Optional.of(new Subject("Re: Test"))),
Arguments.of(Optional.empty(), Optional.of(new MimeMessageId("references2")), Optional.empty(), Optional.of(new Subject("Fwd: Re: Test"))),
// mails related to old message by subject and References
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("Message-ID"))), Optional.of(new Subject("Fwd: Re: Test"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("someInReplyTo"))), Optional.of(new Subject("Fwd: Re: Test"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("references1"), new MimeMessageId("NonRelated-references2"))), Optional.of(new Subject("Fwd: Re: Test"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("NonRelated-references1"), new MimeMessageId("references2"))), Optional.of(new Subject("Fwd: Re: Test"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("references1"), new MimeMessageId("references2"))), Optional.of(new Subject("Fwd: Re: Test")))
);
}
@ParameterizedTest
@MethodSource
void givenOldMailWhenAddNewRelatedMailsThenGuessingThreadIdShouldReturnSameThreadIdWithOldMail(Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject) throws Exception {
// given old mail
MessageManager.AppendResult message = inbox.appendMessage(MessageManager.AppendCommand.from(Message.Builder.of()
.setSubject("Test")
.setMessageId("Message-ID")
.setField(new RawField("In-Reply-To", "someInReplyTo"))
.addField(new RawField("References", "references1"))
.addField(new RawField("References", "references2"))
.setBody("testmail", StandardCharsets.UTF_8)), mailboxSession);
// add new related mails
ThreadId threadId = testee.guessThreadIdReactive(newBasedMessageId, mimeMessageId, inReplyTo, references, subject, mailboxSession).block();
// guessing threadId should return same threadId with old mail
assertThat(threadId).isEqualTo(message.getThreadId());
}
private static Stream<Arguments> givenOldMailWhenAddNewMailsWithRelatedSubjectButHaveNonIdenticalMessageIDThenGuessingThreadIdShouldBasedOnGeneratedMessageId() {
return Stream.of(
// mails related to old message by subject but have non same identical Message-ID
Arguments.of(Optional.of(new MimeMessageId("NonRelated-Message-ID")), Optional.empty(), Optional.empty(), Optional.of(new Subject("Re: Test"))),
Arguments.of(Optional.empty(), Optional.of(new MimeMessageId("NonRelated-someInReplyTo")), Optional.empty(), Optional.of(new Subject("Re: Test"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("NonRelated-references1"), new MimeMessageId("NonRelated-references2"))), Optional.of(new Subject("Re: Test")))
);
}
@ParameterizedTest
@MethodSource
void givenOldMailWhenAddNewMailsWithRelatedSubjectButHaveNonIdenticalMessageIDThenGuessingThreadIdShouldBasedOnGeneratedMessageId(Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject) throws Exception {
// given old mail
MessageManager.AppendResult message = inbox.appendMessage(MessageManager.AppendCommand.from(Message.Builder.of()
.setSubject("Test")
.setMessageId("Message-ID")
.setField(new RawField("In-Reply-To", "someInReplyTo"))
.addField(new RawField("References", "references1"))
.addField(new RawField("References", "references2"))
.setBody("testmail", StandardCharsets.UTF_8)), mailboxSession);
// add mails related to old message by subject but have non same identical Message-ID
ThreadId threadId = testee.guessThreadIdReactive(newBasedMessageId, mimeMessageId, inReplyTo, references, subject, mailboxSession).block();
// guessing threadId should based on generated MessageId
assertThat(threadId.getBaseMessageId()).isEqualTo(newBasedMessageId);
}
private static Stream<Arguments> givenOldMailWhenAddNewMailsWithNonRelatedSubjectButHaveSameIdenticalMessageIDThenGuessingThreadIdShouldBasedOnGeneratedMessageId() {
return Stream.of(
// mails related to old message by having identical Message-ID but non related subject
Arguments.of(Optional.of(new MimeMessageId("Message-ID")), Optional.empty(), Optional.empty(), Optional.of(new Subject("NonRelated-Subject"))),
Arguments.of(Optional.empty(), Optional.of(new MimeMessageId("someInReplyTo")), Optional.empty(), Optional.of(new Subject("NonRelated-Subject"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("references1"), new MimeMessageId("references2"))), Optional.of(new Subject("NonRelated-Subject")))
);
}
@ParameterizedTest
@MethodSource
void givenOldMailWhenAddNewMailsWithNonRelatedSubjectButHaveSameIdenticalMessageIDThenGuessingThreadIdShouldBasedOnGeneratedMessageId(Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject) throws Exception {
// given old mail
MessageManager.AppendResult message = inbox.appendMessage(MessageManager.AppendCommand.from(Message.Builder.of()
.setSubject("Test")
.setMessageId("Message-ID")
.setField(new RawField("In-Reply-To", "someInReplyTo"))
.addField(new RawField("References", "references1"))
.addField(new RawField("References", "references2"))
.setBody("testmail", StandardCharsets.UTF_8)), mailboxSession);
// add mails related to old message by having identical Message-ID but non related subject
ThreadId threadId = testee.guessThreadIdReactive(newBasedMessageId, mimeMessageId, inReplyTo, references, subject, mailboxSession).block();
// guess ThreadId should based on generated MessageId
assertThat(threadId.getBaseMessageId()).isEqualTo(newBasedMessageId);
}
private static Stream<Arguments> givenOldMailWhenAddNonRelatedMailsThenGuessingThreadIdShouldBasedOnGeneratedMessageId() {
return Stream.of(
// mails non related to old message by both subject and identical Message-ID
Arguments.of(Optional.of(new MimeMessageId("NonRelated-Message-ID")), Optional.empty(), Optional.empty(), Optional.of(new Subject("NonRelated-Subject"))),
Arguments.of(Optional.empty(), Optional.of(new MimeMessageId("NonRelated-someInReplyTo")), Optional.empty(), Optional.of(new Subject("NonRelated-Subject"))),
Arguments.of(Optional.empty(), Optional.empty(), Optional.of(List.of(new MimeMessageId("NonRelated-references1"), new MimeMessageId("NonRelated-references2"))), Optional.of(new Subject("NonRelated-Subject")))
);
}
@ParameterizedTest
@MethodSource
void givenOldMailWhenAddNonRelatedMailsThenGuessingThreadIdShouldBasedOnGeneratedMessageId(Optional<MimeMessageId> mimeMessageId, Optional<MimeMessageId> inReplyTo, Optional<List<MimeMessageId>> references, Optional<Subject> subject) throws Exception {
// given old mail
MessageManager.AppendResult message = inbox.appendMessage(MessageManager.AppendCommand.from(Message.Builder.of()
.setSubject("Test")
.setMessageId("Message-ID")
.setField(new RawField("In-Reply-To", "someInReplyTo"))
.addField(new RawField("References", "references1"))
.addField(new RawField("References", "references2"))
.setBody("testmail", StandardCharsets.UTF_8)), mailboxSession);
// add mails non related to old message by both subject and identical Message-ID
ThreadId threadId = testee.guessThreadIdReactive(newBasedMessageId, mimeMessageId, inReplyTo, references, subject, mailboxSession).block();
// guess ThreadId should based on generatedMessageId
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();
}
}