This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 4dd396e1579236bd79dd07872ea0688a3f8fdede Author: Benoit Tellier <[email protected]> AuthorDate: Fri Nov 13 11:57:33 2020 +0700 JAMES-3440 Provide a mailbox listener to populate EmailQueryView --- .../jmap/event/PopulateEmailQueryViewListener.java | 128 ++++++++++++++++ .../event/PopulateEmailQueryViewListenerTest.java | 166 +++++++++++++++++++++ 2 files changed, 294 insertions(+) diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java new file mode 100644 index 0000000..0251576 --- /dev/null +++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java @@ -0,0 +1,128 @@ +/**************************************************************** + * 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.event; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Optional; + +import javax.inject.Inject; + +import org.apache.james.jmap.api.projections.EmailQueryView; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MessageIdManager; +import org.apache.james.mailbox.SessionProvider; +import org.apache.james.mailbox.events.Event; +import org.apache.james.mailbox.events.Group; +import org.apache.james.mailbox.events.MailboxListener.ReactiveGroupMailboxListener; +import org.apache.james.mailbox.model.FetchGroup; +import org.apache.james.mailbox.model.MessageId; +import org.apache.james.mailbox.model.MessageMetaData; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.stream.MimeConfig; +import org.reactivestreams.Publisher; + +import com.github.fge.lambdas.Throwing; +import com.google.common.collect.ImmutableList; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class PopulateEmailQueryViewListener implements ReactiveGroupMailboxListener { + public static class PopulateEmailQueryViewListenerGroup extends Group { + + } + + static final Group GROUP = new PopulateEmailQueryViewListenerGroup(); + private static final int CONCURRENCY = 5; + + private final MessageIdManager messageIdManager; + private final EmailQueryView view; + private final SessionProvider sessionProvider; + + @Inject + public PopulateEmailQueryViewListener(MessageIdManager messageIdManager, EmailQueryView view, SessionProvider sessionProvider) { + this.messageIdManager = messageIdManager; + this.view = view; + this.sessionProvider = sessionProvider; + } + + @Override + public Group getDefaultGroup() { + return GROUP; + } + + @Override + public boolean isHandling(Event event) { + return event instanceof Added + || event instanceof Expunged + || event instanceof MailboxDeletion; + } + + @Override + public Publisher<Void> reactiveEvent(Event event) { + if (event instanceof Added) { + return handleAdded((Added) event); + } + if (event instanceof Expunged) { + return handleExpunged((Expunged) event); + } + if (event instanceof MailboxDeletion) { + return handleMailboxDeletion((MailboxDeletion) event); + } + return Mono.empty(); + } + + public Publisher<Void> handleMailboxDeletion(MailboxDeletion mailboxDeletion) { + return view.delete(mailboxDeletion.getMailboxId()); + } + + public Publisher<Void> handleExpunged(Expunged expunged) { + return Flux.fromStream(expunged.getUids().stream() + .map(uid -> expunged.getMetaData(uid).getMessageId())) + .concatMap(messageId -> view.delete(expunged.getMailboxId(), messageId)) + .then(); + } + + public Mono<Void> handleAdded(Added added) { + MailboxSession session = sessionProvider.createSystemSession(added.getUsername()); + return Flux.fromStream(added.getUids().stream() + .map(added::getMetaData)) + .flatMap(messageMetaData -> handleAdded(added, messageMetaData, session), CONCURRENCY) + .then(); + } + + public Mono<Void> handleAdded(Added added, MessageMetaData messageMetaData, MailboxSession session) { + MessageId messageId = messageMetaData.getMessageId(); + ZonedDateTime receivedAt = ZonedDateTime.ofInstant(messageMetaData.getInternalDate().toInstant(), ZoneOffset.UTC); + + Mono<ZonedDateTime> sentAtMono = Flux.from(messageIdManager.getMessagesReactive(ImmutableList.of(messageId), FetchGroup.HEADERS, session)) + .next() + .map(Throwing.function(messageResult -> Message.Builder + .of() + .use(MimeConfig.PERMISSIVE) + .parse(messageResult.getFullContent().getInputStream()) + .build())) + .map(message -> Optional.ofNullable(message.getDate()).orElse(messageMetaData.getInternalDate())) + .map(date -> ZonedDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC)); + + return sentAtMono.flatMap(sentAt -> view.save(added.getMailboxId(), sentAt, receivedAt, messageId)); + } +} diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/event/PopulateEmailQueryViewListenerTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/event/PopulateEmailQueryViewListenerTest.java new file mode 100644 index 0000000..a8a9293 --- /dev/null +++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/event/PopulateEmailQueryViewListenerTest.java @@ -0,0 +1,166 @@ +/**************************************************************** + * 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.event; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Date; + +import org.apache.james.core.Username; +import org.apache.james.jmap.memory.projections.MemoryEmailQueryView; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MailboxSessionUtil; +import org.apache.james.mailbox.MessageIdManager; +import org.apache.james.mailbox.MessageManager; +import org.apache.james.mailbox.events.Group; +import org.apache.james.mailbox.events.InVMEventBus; +import org.apache.james.mailbox.events.MemoryEventDeadLetters; +import org.apache.james.mailbox.events.RetryBackoffConfiguration; +import org.apache.james.mailbox.events.delivery.InVmEventDelivery; +import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources; +import org.apache.james.mailbox.model.ComposedMessageId; +import org.apache.james.mailbox.model.MailboxId; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.store.FakeAuthenticator; +import org.apache.james.mailbox.store.FakeAuthorizator; +import org.apache.james.mailbox.store.SessionProviderImpl; +import org.apache.james.mailbox.store.StoreMailboxManager; +import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.util.streams.Limit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +public class PopulateEmailQueryViewListenerTest { + private static final Username BOB = Username.of("bob"); + private static final MailboxPath BOB_INBOX_PATH = MailboxPath.inbox(BOB); + private static final MailboxPath BOB_OTHER_BOX_PATH = MailboxPath.forUser(BOB, "otherBox"); + + MailboxSession mailboxSession; + StoreMailboxManager mailboxManager; + + MessageManager inboxMessageManager; + MessageManager otherBoxMessageManager; + PopulateEmailQueryViewListener listener; + MessageIdManager messageIdManager; + private MemoryEmailQueryView view; + private MailboxId inboxId; + + @BeforeEach + void setup() throws Exception { + // Default RetryBackoffConfiguration leads each events to be re-executed for 30s which is too long + // Reducing the wait time for the event bus allow a faster test suite execution without harming test correctness + RetryBackoffConfiguration backoffConfiguration = RetryBackoffConfiguration.builder() + .maxRetries(2) + .firstBackoff(Duration.ofMillis(1)) + .jitterFactor(0.5) + .build(); + InMemoryIntegrationResources resources = InMemoryIntegrationResources.builder() + .preProvisionnedFakeAuthenticator() + .fakeAuthorizator() + .eventBus(new InVMEventBus(new InVmEventDelivery(new RecordingMetricFactory()), backoffConfiguration, new MemoryEventDeadLetters())) + .defaultAnnotationLimits() + .defaultMessageParser() + .scanningSearchIndex() + .noPreDeletionHooks() + .storeQuotaManager() + .build(); + + mailboxManager = resources.getMailboxManager(); + messageIdManager = resources.getMessageIdManager(); + + FakeAuthenticator authenticator = new FakeAuthenticator(); + authenticator.addUser(BOB, "12345"); + SessionProviderImpl sessionProvider = new SessionProviderImpl(authenticator, FakeAuthorizator.defaultReject()); + + view = new MemoryEmailQueryView(); + listener = new PopulateEmailQueryViewListener(messageIdManager, view, sessionProvider); + + resources.getEventBus().register(listener); + + mailboxSession = MailboxSessionUtil.create(BOB); + + inboxId = mailboxManager.createMailbox(BOB_INBOX_PATH, mailboxSession).get(); + inboxMessageManager = mailboxManager.getMailbox(inboxId, mailboxSession); + + MailboxId otherBoxId = mailboxManager.createMailbox(BOB_OTHER_BOX_PATH, mailboxSession).get(); + otherBoxMessageManager = mailboxManager.getMailbox(otherBoxId, mailboxSession); + } + + + @Test + void deserializeMailboxAnnotationListenerGroup() throws Exception { + assertThat(Group.deserialize("org.apache.james.jmap.event.PopulateEmailQueryViewListener$PopulateEmailQueryViewListenerGroup")) + .isEqualTo(new PopulateEmailQueryViewListener.PopulateEmailQueryViewListenerGroup()); + } + + @Test + void appendingAMessageShouldAddItToTheView() throws Exception { + ComposedMessageId composedId = inboxMessageManager.appendMessage( + MessageManager.AppendCommand.builder() + .withInternalDate(Date.from(ZonedDateTime.parse("2014-10-30T15:12:00Z").toInstant())) + .build(emptyMessage(Date.from(ZonedDateTime.parse("2014-10-30T14:12:00Z").toInstant()))), + mailboxSession).getId(); + + assertThat(view.listMailboxContent(inboxId, Limit.limit(12)).collectList().block()) + .containsOnly(composedId.getMessageId()); + } + + @Test + void deletingMailboxShouldClearTheView() throws Exception { + inboxMessageManager.appendMessage( + MessageManager.AppendCommand.builder() + .withInternalDate(Date.from(ZonedDateTime.parse("2014-10-30T15:12:00Z").toInstant())) + .build(emptyMessage(Date.from(ZonedDateTime.parse("2014-10-30T14:12:00Z").toInstant()))), + mailboxSession).getId(); + + mailboxManager.deleteMailbox(inboxId, mailboxSession); + + assertThat(view.listMailboxContent(inboxId, Limit.limit(12)).collectList().block()) + .isEmpty(); + } + + @Test + void deletingEmailShouldClearTheView() throws Exception { + ComposedMessageId composedMessageId = inboxMessageManager.appendMessage( + MessageManager.AppendCommand.builder() + .withInternalDate(Date.from(ZonedDateTime.parse("2014-10-30T15:12:00Z").toInstant())) + .build(emptyMessage(Date.from(ZonedDateTime.parse("2014-10-30T14:12:00Z").toInstant()))), + mailboxSession).getId(); + + inboxMessageManager.delete(ImmutableList.of(composedMessageId.getUid()), mailboxSession); + + assertThat(view.listMailboxContent(inboxId, Limit.limit(12)).collectList().block()) + .isEmpty(); + } + + private Message emptyMessage(Date sentAt) throws Exception { + return Message.Builder.of() + .setSubject("Empty message") + .setDate(sentAt) + .setBody("", StandardCharsets.UTF_8) + .build(); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
