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 2023/03/28 08:23:18 UTC

[james-project] branch master updated: JAMES-3885 Change username - migrate quotas (#1500)

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


The following commit(s) were added to refs/heads/master by this push:
     new 96975734cc JAMES-3885 Change username - migrate quotas (#1500)
96975734cc is described below

commit 96975734ccc0178de92bcd48559f76e2f32e64d1
Author: vttran <vt...@linagora.com>
AuthorDate: Tue Mar 28 15:23:10 2023 +0700

    JAMES-3885 Change username - migrate quotas (#1500)
---
 .../docs/modules/ROOT/pages/operate/webadmin.adoc  |   1 +
 .../modules/mailbox/CassandraMailboxModule.java    |   2 +
 .../james/modules/mailbox/JPAMailboxModule.java    |   2 +
 .../james/modules/mailbox/MemoryMailboxModule.java |   2 +
 .../mailbox/QuotaUsernameChangeTaskStep.java       | 126 +++++++++++
 .../mailbox/QuotaUsernameChangeTaskStepTest.java   | 252 +++++++++++++++++++++
 .../MemoryUsernameChangeIntegrationTest.java       |  65 ++++++
 src/site/markdown/server/manage-webadmin.md        |   2 +-
 8 files changed, 451 insertions(+), 1 deletion(-)

diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
index ad8936c269..0fcca321ba 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
@@ -638,6 +638,7 @@ Implemented migration steps are:
  - `MailboxUsernameChangeTaskStep`: migrates mailboxes belonging to the old user to the account of the new user. It also
  migrates user's mailbox subscriptions.
  - `ACLUsernameChangeTaskStep`: migrates ACLs on mailboxes the migrated user has access to and updates subscriptions accordingly.
+ - `QuotaUsernameChangeTaskStep`: migrates quotas user from old user to new user.
 
 Response codes:
 
diff --git a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java
index cf38c919b4..cd0a699521 100644
--- a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java
+++ b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java
@@ -24,6 +24,7 @@ import javax.inject.Singleton;
 
 import org.apache.james.adapter.mailbox.ACLUsernameChangeTaskStep;
 import org.apache.james.adapter.mailbox.MailboxUsernameChangeTaskStep;
+import org.apache.james.adapter.mailbox.QuotaUsernameChangeTaskStep;
 import org.apache.james.adapter.mailbox.UserRepositoryAuthenticator;
 import org.apache.james.backends.cassandra.components.CassandraModule;
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager;
@@ -247,6 +248,7 @@ public class CassandraMailboxModule extends AbstractModule {
         Multibinder<UsernameChangeTaskStep> usernameChangeTaskStepMultibinder = Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class);
         usernameChangeTaskStepMultibinder.addBinding().to(MailboxUsernameChangeTaskStep.class);
         usernameChangeTaskStepMultibinder.addBinding().to(ACLUsernameChangeTaskStep.class);
+        usernameChangeTaskStepMultibinder.addBinding().to(QuotaUsernameChangeTaskStep.class);
     }
 
     @Provides
diff --git a/server/container/guice/mailbox-jpa/src/main/java/org/apache/james/modules/mailbox/JPAMailboxModule.java b/server/container/guice/mailbox-jpa/src/main/java/org/apache/james/modules/mailbox/JPAMailboxModule.java
index 62138140bf..5bf648d6d8 100644
--- a/server/container/guice/mailbox-jpa/src/main/java/org/apache/james/modules/mailbox/JPAMailboxModule.java
+++ b/server/container/guice/mailbox-jpa/src/main/java/org/apache/james/modules/mailbox/JPAMailboxModule.java
@@ -24,6 +24,7 @@ import javax.inject.Singleton;
 
 import org.apache.james.adapter.mailbox.ACLUsernameChangeTaskStep;
 import org.apache.james.adapter.mailbox.MailboxUsernameChangeTaskStep;
+import org.apache.james.adapter.mailbox.QuotaUsernameChangeTaskStep;
 import org.apache.james.adapter.mailbox.UserRepositoryAuthenticator;
 import org.apache.james.adapter.mailbox.UserRepositoryAuthorizator;
 import org.apache.james.events.EventListener;
@@ -128,6 +129,7 @@ public class JPAMailboxModule extends AbstractModule {
         Multibinder<UsernameChangeTaskStep> usernameChangeTaskStepMultibinder = Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class);
         usernameChangeTaskStepMultibinder.addBinding().to(MailboxUsernameChangeTaskStep.class);
         usernameChangeTaskStepMultibinder.addBinding().to(ACLUsernameChangeTaskStep.class);
+        usernameChangeTaskStepMultibinder.addBinding().to(QuotaUsernameChangeTaskStep.class);
     }
     
     @Singleton
diff --git a/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java b/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
index 033d4daccb..61083eb515 100644
--- a/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
+++ b/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryMailboxModule.java
@@ -26,6 +26,7 @@ import javax.inject.Singleton;
 import org.apache.james.adapter.mailbox.ACLUsernameChangeTaskStep;
 import org.apache.james.adapter.mailbox.DelegationStoreAuthorizator;
 import org.apache.james.adapter.mailbox.MailboxUsernameChangeTaskStep;
+import org.apache.james.adapter.mailbox.QuotaUsernameChangeTaskStep;
 import org.apache.james.adapter.mailbox.UserRepositoryAuthenticator;
 import org.apache.james.events.EventListener;
 import org.apache.james.jmap.api.change.EmailChangeRepository;
@@ -160,6 +161,7 @@ public class MemoryMailboxModule extends AbstractModule {
         Multibinder<UsernameChangeTaskStep> usernameChangeTaskStepMultibinder = Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class);
         usernameChangeTaskStepMultibinder.addBinding().to(MailboxUsernameChangeTaskStep.class);
         usernameChangeTaskStepMultibinder.addBinding().to(ACLUsernameChangeTaskStep.class);
+        usernameChangeTaskStepMultibinder.addBinding().to(QuotaUsernameChangeTaskStep.class);
     }
 
     @Singleton
diff --git a/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/QuotaUsernameChangeTaskStep.java b/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/QuotaUsernameChangeTaskStep.java
new file mode 100644
index 0000000000..75738437bb
--- /dev/null
+++ b/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/QuotaUsernameChangeTaskStep.java
@@ -0,0 +1,126 @@
+/****************************************************************
+ * 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.adapter.mailbox;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.events.EventBus;
+import org.apache.james.events.RegistrationKey;
+import org.apache.james.mailbox.model.CurrentQuotas;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.model.QuotaOperation;
+import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mailbox.quota.CurrentQuotaManager;
+import org.apache.james.mailbox.quota.MaxQuotaManager;
+import org.apache.james.mailbox.quota.QuotaManager;
+import org.apache.james.mailbox.quota.UserQuotaRootResolver;
+import org.apache.james.mailbox.store.event.EventFactory;
+import org.apache.james.user.api.UsernameChangeTaskStep;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.ImmutableSet;
+
+import reactor.core.publisher.Mono;
+
+public class QuotaUsernameChangeTaskStep implements UsernameChangeTaskStep {
+    private static final ImmutableSet<RegistrationKey> NO_REGISTRATION_KEYS = ImmutableSet.of();
+    private final QuotaManager quotaManager;
+    private final MaxQuotaManager maxQuotaManager;
+    private final CurrentQuotaManager currentQuotaManager;
+    private final UserQuotaRootResolver userQuotaRootResolver;
+    private final EventBus eventBus;
+
+    @Inject
+    public QuotaUsernameChangeTaskStep(QuotaManager quotaManager,
+                                       CurrentQuotaManager currentQuotaManager,
+                                       UserQuotaRootResolver userQuotaRootResolver,
+                                       MaxQuotaManager maxQuotaManager,
+                                       EventBus eventBus) {
+        this.quotaManager = quotaManager;
+        this.currentQuotaManager = currentQuotaManager;
+        this.userQuotaRootResolver = userQuotaRootResolver;
+        this.maxQuotaManager = maxQuotaManager;
+        this.eventBus = eventBus;
+    }
+
+    @Override
+    public StepName name() {
+        return new StepName("QuotaUsernameChangeTaskStep");
+    }
+
+    @Override
+    public int priority() {
+        return 3;
+    }
+
+    @Override
+    public Publisher<Void> changeUsername(Username oldUsername, Username newUsername) {
+        return Mono.from(quotaManager.getQuotasReactive(userQuotaRootResolver.forUser(oldUsername)))
+            .flatMap(quotas -> Mono.fromCallable(() -> userQuotaRootResolver.forUser(newUsername))
+                .flatMap(newUserQuotaRoot -> setQuotaForNewUser(newUserQuotaRoot, quotas)
+                    .then(dispatchNewEventQuota(newUserQuotaRoot, newUsername))));
+    }
+
+    private Mono<Void> setQuotaForNewUser(QuotaRoot quotaRoot, QuotaManager.Quotas quotas) {
+        return setMaxQuota(quotaRoot, quotas)
+            .then(setCurrentQuota(quotaRoot, quotas));
+    }
+
+    private Mono<Void> setMaxQuota(QuotaRoot quotaRoot, QuotaManager.Quotas quotas) {
+        return Mono.zip(setMaxMessagesQuota(quotaRoot, quotas).thenReturn(quotaRoot),
+                setMaxStorageQuota(quotaRoot, quotas).thenReturn(quotaRoot))
+            .then();
+    }
+
+    private Mono<Void> setMaxStorageQuota(QuotaRoot quotaRoot, QuotaManager.Quotas quotas) {
+        return Mono.justOrEmpty(Optional.ofNullable(quotas.getStorageQuota().getLimitByScope()
+                .get(Quota.Scope.User)))
+            .flatMap(quotaSizeLimit -> Mono.from(maxQuotaManager.setMaxStorageReactive(quotaRoot, quotaSizeLimit)));
+    }
+
+    private Mono<Void> setMaxMessagesQuota(QuotaRoot quotaRoot, QuotaManager.Quotas quotas) {
+        return Mono.justOrEmpty(Optional.ofNullable(quotas.getMessageQuota().getLimitByScope()
+                .get(Quota.Scope.User)))
+            .flatMap(quotaCountLimit -> Mono.from(maxQuotaManager.setMaxMessageReactive(quotaRoot, quotaCountLimit)));
+    }
+
+    private Mono<Void> setCurrentQuota(QuotaRoot quotaRoot, QuotaManager.Quotas quotas) {
+        return Mono.from(currentQuotaManager.setCurrentQuotas(QuotaOperation.from(quotaRoot,
+            new CurrentQuotas(quotas.getMessageQuota().getUsed(), quotas.getStorageQuota().getUsed()))));
+    }
+
+    private Mono<Void> dispatchNewEventQuota(QuotaRoot quotaRoot, Username username) {
+        return Mono.from(quotaManager.getQuotasReactive(quotaRoot))
+            .flatMap(quotas -> eventBus.dispatch(
+                EventFactory.quotaUpdated()
+                    .randomEventId()
+                    .user(username)
+                    .quotaRoot(quotaRoot)
+                    .quotaCount(quotas.getMessageQuota())
+                    .quotaSize(quotas.getStorageQuota())
+                    .instant(Instant.now())
+                    .build(),
+                NO_REGISTRATION_KEYS));
+    }
+}
diff --git a/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/QuotaUsernameChangeTaskStepTest.java b/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/QuotaUsernameChangeTaskStepTest.java
new file mode 100644
index 0000000000..5ea415be8c
--- /dev/null
+++ b/server/container/mailbox-adapter/src/test/java/org/apache/james/adapter/mailbox/QuotaUsernameChangeTaskStepTest.java
@@ -0,0 +1,252 @@
+/****************************************************************
+ * 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.adapter.mailbox;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.awaitility.Durations.TEN_SECONDS;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.james.core.Username;
+import org.apache.james.core.quota.QuotaCountLimit;
+import org.apache.james.core.quota.QuotaCountUsage;
+import org.apache.james.core.quota.QuotaSizeLimit;
+import org.apache.james.core.quota.QuotaSizeUsage;
+import org.apache.james.events.Event;
+import org.apache.james.events.EventBus;
+import org.apache.james.events.EventBusTestFixture;
+import org.apache.james.events.EventListener;
+import org.apache.james.events.Group;
+import org.apache.james.mailbox.events.GenericGroup;
+import org.apache.james.mailbox.events.MailboxEvents;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.CurrentQuotas;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.model.QuotaOperation;
+import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mailbox.quota.CurrentQuotaManager;
+import org.apache.james.mailbox.quota.MaxQuotaManager;
+import org.apache.james.mailbox.quota.QuotaManager;
+import org.apache.james.mailbox.quota.UserQuotaRootResolver;
+import org.apache.james.mailbox.store.event.EventFactory;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableSet;
+
+import reactor.core.publisher.Mono;
+
+class QuotaUsernameChangeTaskStepTest {
+    private static final Username ALICE = Username.of("alice");
+    private static final Username BOB = Username.of("bob");
+
+    private QuotaUsernameChangeTaskStep testee;
+    private QuotaManager quotaManager;
+    private CurrentQuotaManager currentQuotaManager;
+    private UserQuotaRootResolver quotaRootResolver;
+    private MaxQuotaManager maxQuotaManager;
+
+    private ArrayList<Event> eventStore;
+
+    @BeforeEach
+    void setUp() {
+        InMemoryIntegrationResources resources = InMemoryIntegrationResources.defaultResources();
+        quotaManager = resources.getQuotaManager();
+        currentQuotaManager = resources.getCurrentQuotaManager();
+        quotaRootResolver = resources.getDefaultUserQuotaRootResolver();
+        maxQuotaManager = resources.getMaxQuotaManager();
+
+        eventStore  = new ArrayList<>();
+
+        EventBus eventBus = resources.getEventBus();
+
+        eventBus.register(new EventListener.GroupEventListener() {
+            @Override
+            public Group getDefaultGroup() {
+                return new GenericGroup("test");
+            }
+
+            @Override
+            public void event(Event event) {
+                eventStore.add(event);
+            }
+        });
+
+        testee = new QuotaUsernameChangeTaskStep(
+            quotaManager,
+            currentQuotaManager,
+            quotaRootResolver,
+            maxQuotaManager,
+            eventBus);
+    }
+
+    @Test
+    void shouldMigrateQuotas() throws Exception {
+        QuotaRoot bobQuotaRoot = quotaRootResolver.forUser(BOB);
+        maxQuotaManager.setMaxMessage(bobQuotaRoot, QuotaCountLimit.count(50));
+        maxQuotaManager.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.size(100));
+        Mono.from(currentQuotaManager.setCurrentQuotas(QuotaOperation.from(bobQuotaRoot, new CurrentQuotas(
+            QuotaCountUsage.count(5), QuotaSizeUsage.size(10)
+        )))).block();
+
+        Mono.from(testee.changeUsername(BOB, ALICE)).block();
+
+        QuotaManager.Quotas aliceQuotas = quotaManager.getQuotas(quotaRootResolver.forUser(ALICE));
+
+        assertSoftly(softly -> {
+            softly.assertThat(aliceQuotas.getMessageQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(5)).computedLimit(QuotaCountLimit.count(50)).build());
+
+            softly.assertThat(aliceQuotas.getStorageQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(10)).computedLimit(QuotaSizeLimit.size(100)).build());
+        });
+    }
+
+    @Test
+    void migrateShouldNotThrowWhenNoQuotas() throws Exception {
+        assertThatCode(() -> Mono.from(testee.changeUsername(BOB, ALICE)).block())
+            .doesNotThrowAnyException();
+        QuotaManager.Quotas aliceQuotas = quotaManager.getQuotas(quotaRootResolver.forUser(ALICE));
+        assertSoftly(softly -> {
+            softly.assertThat(aliceQuotas.getMessageQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(0)).computedLimit(QuotaCountLimit.unlimited()).build());
+
+            softly.assertThat(aliceQuotas.getStorageQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(0)).computedLimit(QuotaSizeLimit.unlimited()).build());
+        });
+    }
+
+    @Test
+    void migrateShouldSucceedWhenUnlimitedQuotas() throws Exception {
+        QuotaRoot bobQuotaRoot = quotaRootResolver.forUser(BOB);
+        maxQuotaManager.setMaxMessage(bobQuotaRoot, QuotaCountLimit.unlimited());
+        maxQuotaManager.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.unlimited());
+        Mono.from(currentQuotaManager.setCurrentQuotas(QuotaOperation.from(bobQuotaRoot, new CurrentQuotas(
+            QuotaCountUsage.count(5), QuotaSizeUsage.size(10)
+        )))).block();
+
+        Mono.from(testee.changeUsername(BOB, ALICE)).block();
+
+        QuotaManager.Quotas aliceQuotas = quotaManager.getQuotas(quotaRootResolver.forUser(ALICE));
+        assertSoftly(softly -> {
+            softly.assertThat(aliceQuotas.getMessageQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(5)).computedLimit(QuotaCountLimit.unlimited()).build());
+
+            softly.assertThat(aliceQuotas.getStorageQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(10)).computedLimit(QuotaSizeLimit.unlimited()).build());
+        });
+    }
+
+    @Test
+    void migrateShouldSucceedWhenOnlyMessagesQuota() throws Exception {
+        QuotaRoot bobQuotaRoot = quotaRootResolver.forUser(BOB);
+        maxQuotaManager.setMaxMessage(bobQuotaRoot, QuotaCountLimit.count(10));
+        Mono.from(testee.changeUsername(BOB, ALICE)).block();
+
+        QuotaManager.Quotas aliceQuotas = quotaManager.getQuotas(quotaRootResolver.forUser(ALICE));
+        assertSoftly(softly -> {
+            softly.assertThat(aliceQuotas.getMessageQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(0)).computedLimit(QuotaCountLimit.count(10)).build());
+
+            softly.assertThat(aliceQuotas.getStorageQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(0)).computedLimit(QuotaSizeLimit.unlimited()).build());
+        });
+    }
+
+    @Test
+    void migrateShouldSucceedWhenOnlyStorageQuota() throws Exception {
+        QuotaRoot bobQuotaRoot = quotaRootResolver.forUser(BOB);
+        maxQuotaManager.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.size(10));
+        Mono.from(testee.changeUsername(BOB, ALICE)).block();
+
+        QuotaManager.Quotas aliceQuotas = quotaManager.getQuotas(quotaRootResolver.forUser(ALICE));
+        assertSoftly(softly -> {
+            softly.assertThat(aliceQuotas.getMessageQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(0)).computedLimit(QuotaCountLimit.unlimited()).build());
+
+            softly.assertThat(aliceQuotas.getStorageQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(0)).computedLimit(QuotaSizeLimit.size(10)).build());
+        });
+    }
+
+    @Test
+    void migrateShouldSucceedWhenAliceAlreadyQuotas() throws Exception {
+        QuotaRoot bobQuotaRoot = quotaRootResolver.forUser(BOB);
+        maxQuotaManager.setMaxMessage(bobQuotaRoot, QuotaCountLimit.count(50));
+        maxQuotaManager.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.size(100));
+        Mono.from(currentQuotaManager.setCurrentQuotas(QuotaOperation.from(bobQuotaRoot, new CurrentQuotas(
+            QuotaCountUsage.count(5), QuotaSizeUsage.size(10)
+        )))).block();
+
+        QuotaRoot aliceQuotaRoot = quotaRootResolver.forUser(ALICE);
+        maxQuotaManager.setMaxMessage(aliceQuotaRoot, QuotaCountLimit.count(55));
+        maxQuotaManager.setMaxStorage(aliceQuotaRoot, QuotaSizeLimit.size(150));
+        Mono.from(currentQuotaManager.setCurrentQuotas(QuotaOperation.from(aliceQuotaRoot, new CurrentQuotas(
+            QuotaCountUsage.count(7), QuotaSizeUsage.size(8)
+        )))).block();
+
+        Mono.from(testee.changeUsername(BOB, ALICE)).block();
+
+        QuotaManager.Quotas aliceQuotas = quotaManager.getQuotas(quotaRootResolver.forUser(ALICE));
+
+        assertSoftly(softly -> {
+            softly.assertThat(aliceQuotas.getMessageQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(5)).computedLimit(QuotaCountLimit.count(50)).build());
+
+            softly.assertThat(aliceQuotas.getStorageQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(10)).computedLimit(QuotaSizeLimit.size(100)).build());
+        });
+    }
+
+    @Test
+    void migrateShouldDispatchQuotaUpdateEvent() throws Exception {
+        QuotaRoot bobQuotaRoot = quotaRootResolver.forUser(BOB);
+        maxQuotaManager.setMaxMessage(bobQuotaRoot, QuotaCountLimit.count(50));
+        maxQuotaManager.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.size(100));
+        Mono.from(currentQuotaManager.setCurrentQuotas(QuotaOperation.from(bobQuotaRoot, new CurrentQuotas(
+            QuotaCountUsage.count(5), QuotaSizeUsage.size(10)
+        )))).block();
+
+        Mono.from(testee.changeUsername(BOB, ALICE)).block();
+
+
+        Awaitility.await()
+            .atMost(TEN_SECONDS)
+            .untilAsserted(() -> assertThat(eventStore.size()).isEqualTo(1));
+
+        MailboxEvents.QuotaUsageUpdatedEvent quotaUsageUpdatedEvent = (MailboxEvents.QuotaUsageUpdatedEvent) eventStore.get(0);
+
+        assertSoftly(softly -> {
+            softly.assertThat(quotaUsageUpdatedEvent.getCountQuota())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(5)).computedLimit(QuotaCountLimit.count(50)).build());
+            softly.assertThat(quotaUsageUpdatedEvent.getSizeQuota())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(10)).computedLimit(QuotaSizeLimit.size(100)).build());
+            softly.assertThat(quotaUsageUpdatedEvent.getUsername())
+                .isEqualTo(ALICE);
+            softly.assertThat(quotaUsageUpdatedEvent.getQuotaRoot())
+                .isEqualTo(quotaRootResolver.forUser(ALICE));
+        });
+    }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
index 8fb644729a..bd0ea5e116 100644
--- a/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
+++ b/server/protocols/webadmin-integration-test/memory-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/memory/MemoryUsernameChangeIntegrationTest.java
@@ -29,29 +29,42 @@ import static org.apache.james.jmap.JMAPTestingConstants.CEDRIC_PASSWORD;
 import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN;
 import static org.apache.james.jmap.JMAPTestingConstants.jmapRequestSpecBuilder;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.awaitility.Durations.TEN_SECONDS;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.Matchers.hasSize;
 
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Optional;
 
 import javax.inject.Inject;
 
+import org.apache.http.HttpStatus;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
 import org.apache.james.MemoryJamesConfiguration;
 import org.apache.james.MemoryJamesServerMain;
 import org.apache.james.core.Username;
+import org.apache.james.core.quota.QuotaCountLimit;
+import org.apache.james.core.quota.QuotaCountUsage;
+import org.apache.james.core.quota.QuotaSizeLimit;
+import org.apache.james.core.quota.QuotaSizeUsage;
 import org.apache.james.jmap.api.filtering.FilteringManagement;
 import org.apache.james.jmap.api.filtering.Rule;
 import org.apache.james.jmap.api.filtering.Rules;
 import org.apache.james.jmap.api.filtering.Version;
 import org.apache.james.jmap.draft.JmapGuiceProbe;
+import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.model.MailboxACL;
 import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mime4j.dom.Message;
 import org.apache.james.modules.ACLProbeImpl;
 import org.apache.james.modules.MailboxProbeImpl;
+import org.apache.james.modules.QuotaProbesImpl;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.apache.james.probe.DataProbe;
 import org.apache.james.util.Port;
@@ -59,10 +72,13 @@ import org.apache.james.utils.DataProbeImpl;
 import org.apache.james.utils.GuiceProbe;
 import org.apache.james.utils.WebAdminGuiceProbe;
 import org.apache.james.webadmin.WebAdminUtils;
+import org.awaitility.Awaitility;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
+import com.github.fge.lambdas.Throwing;
 import com.google.inject.multibindings.Multibinder;
 
 import io.restassured.RestAssured;
@@ -228,4 +244,53 @@ class MemoryUsernameChangeIntegrationTest {
         assertThat(filterProbe.listRulesForUser(ALICE).getRules())
             .isEmpty();
     }
+
+    @Test
+    void shouldAdaptQuotas(GuiceJamesServer server) throws Exception {
+        server.getProbe(MailboxProbeImpl.class).createMailbox(MailboxPath.inbox(BOB));
+
+        QuotaProbesImpl quotaProbes = server.getProbe(QuotaProbesImpl.class);
+        QuotaRoot bobQuotaRoot = quotaProbes.getQuotaRoot(MailboxPath.inbox(BOB));
+
+        quotaProbes.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(50));
+        quotaProbes.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.size(1000));
+
+        server.getProbe(MailboxProbeImpl.class)
+            .appendMessage(BOB.asString(), MailboxPath.inbox(BOB),
+                MessageManager.AppendCommand.from(Message.Builder.of()
+                    .setSubject("test")
+                    .setBody("testmail", StandardCharsets.UTF_8)
+                    .build()));
+
+        Awaitility.await()
+            .atMost(TEN_SECONDS)
+            .untilAsserted(() -> {
+                assertThat(quotaProbes.getMessageCountQuota(bobQuotaRoot)
+                    .getUsed().asLong())
+                    .isEqualTo(1L);
+                assertThat(quotaProbes.getStorageQuota(bobQuotaRoot)
+                    .getUsed().asLong())
+                    .isEqualTo(85L);
+            });
+
+        String taskId = webAdminApi
+            .queryParam("action", "rename")
+            .post("/users/" + BOB.asString() + "/rename/" + ALICE.asString())
+            .jsonPath()
+            .get("taskId");
+
+        webAdminApi.get("/tasks/" + taskId + "/await")
+            .then()
+            .statusCode(HttpStatus.SC_OK)
+            .body("additionalInformation.status.QuotaUsernameChangeTaskStep", Matchers.is("DONE"));
+
+        QuotaRoot aliceQuotaRoot = quotaProbes.getQuotaRoot(MailboxPath.inbox(ALICE));
+
+        assertSoftly(softly -> {
+            softly.assertThat(Throwing.supplier(() -> quotaProbes.getMessageCountQuota(aliceQuotaRoot)).get())
+                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(1)).computedLimit(QuotaCountLimit.count(50)).build());
+            softly.assertThat(Throwing.supplier(() -> quotaProbes.getStorageQuota(aliceQuotaRoot)).get())
+                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(85)).computedLimit(QuotaSizeLimit.size(1000)).build());
+        });
+    }
 }
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index a3edcf7f75..14eadcc942 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -490,7 +490,7 @@ Implemented migration steps are:
  - `MailboxUsernameChangeTaskStep`: migrates mailboxes belonging to the old user to the account of the new user. It also
  migrates user's mailbox subscriptions.
  - `ACLUsernameChangeTaskStep`: migrates ACLs on mailboxes the migrated user has access to and updates subscriptions accordingly.
-
+ - `QuotaUsernameChangeTaskStep`: migrates quotas user from old user to new user.
 Response codes:
 
 * 201: Success. Corresponding task id is returned.


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org