You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/11/16 12:09:29 UTC

[james-project] 06/08: JAMES-3440 Provide a mailbox listener to populate EmailQueryView

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 <bt...@linagora.com>
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: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org