You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2019/01/16 06:59:20 UTC

[12/17] james-project git commit: JAMES-2637 add rest of the routes for alias routes

JAMES-2637 add rest of the routes for alias routes


Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/b312b877
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/b312b877
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/b312b877

Branch: refs/heads/master
Commit: b312b877cf92e2930d15f222462e97c7bd1799fc
Parents: 5b9b106
Author: Rene Cordier <rc...@linagora.com>
Authored: Thu Jan 10 17:33:34 2019 +0700
Committer: Benoit Tellier <bt...@linagora.com>
Committed: Wed Jan 16 13:48:56 2019 +0700

----------------------------------------------------------------------
 .../rrt/api/MappingAlreadyExistsException.java  |   4 -
 .../api/SameSourceAndDestinationException.java  |   4 -
 .../integration/UnauthorizedEndpointsTest.java  |   3 +
 .../webadmin/dto/AliasSourcesResponse.java      |  32 ++
 .../james/webadmin/routes/AliasRoutes.java      |  95 +++++-
 .../james/webadmin/routes/AliasRoutesTest.java  | 299 +++++++++++++++++--
 src/site/markdown/server/manage-webadmin.md     |  53 ++++
 7 files changed, 457 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/server/data/data-api/src/main/java/org/apache/james/rrt/api/MappingAlreadyExistsException.java
----------------------------------------------------------------------
diff --git a/server/data/data-api/src/main/java/org/apache/james/rrt/api/MappingAlreadyExistsException.java b/server/data/data-api/src/main/java/org/apache/james/rrt/api/MappingAlreadyExistsException.java
index 4d80ad8..ef0d49f 100644
--- a/server/data/data-api/src/main/java/org/apache/james/rrt/api/MappingAlreadyExistsException.java
+++ b/server/data/data-api/src/main/java/org/apache/james/rrt/api/MappingAlreadyExistsException.java
@@ -23,8 +23,4 @@ public class MappingAlreadyExistsException extends RecipientRewriteTableExceptio
     public MappingAlreadyExistsException(String msg) {
         super(msg);
     }
-
-    public MappingAlreadyExistsException(String msg, Throwable t) {
-        super(msg, t);
-    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/server/data/data-api/src/main/java/org/apache/james/rrt/api/SameSourceAndDestinationException.java
----------------------------------------------------------------------
diff --git a/server/data/data-api/src/main/java/org/apache/james/rrt/api/SameSourceAndDestinationException.java b/server/data/data-api/src/main/java/org/apache/james/rrt/api/SameSourceAndDestinationException.java
index d76ffcb..c675c2c 100644
--- a/server/data/data-api/src/main/java/org/apache/james/rrt/api/SameSourceAndDestinationException.java
+++ b/server/data/data-api/src/main/java/org/apache/james/rrt/api/SameSourceAndDestinationException.java
@@ -24,8 +24,4 @@ public class SameSourceAndDestinationException extends RecipientRewriteTableExce
     public SameSourceAndDestinationException(String msg) {
         super(msg);
     }
-
-    public SameSourceAndDestinationException(String msg, Throwable t) {
-        super(msg, t);
-    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/UnauthorizedEndpointsTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/UnauthorizedEndpointsTest.java b/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/UnauthorizedEndpointsTest.java
index f20b041..14abe53 100644
--- a/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/UnauthorizedEndpointsTest.java
+++ b/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/UnauthorizedEndpointsTest.java
@@ -79,6 +79,8 @@ class UnauthorizedEndpointsTest {
             UserRoutes.USERS,
             ForwardRoutes.ROOT_PATH,
             ForwardRoutes.ROOT_PATH + "/alice@james.org",
+            AliasRoutes.ROOT_PATH,
+            AliasRoutes.ROOT_PATH + "/bob@james.org",
             GlobalQuotaRoutes.QUOTA_ENDPOINT,
             GlobalQuotaRoutes.QUOTA_ENDPOINT + "/count",
             GlobalQuotaRoutes.QUOTA_ENDPOINT + "/size",
@@ -157,6 +159,7 @@ class UnauthorizedEndpointsTest {
             UserQuotaRoutes.USERS_QUOTA_ENDPOINT + "/joe@perdu.com/size",
             UserRoutes.USERS + "/user@james.org",
             ForwardRoutes.ROOT_PATH + "/alice@james.org/bob@james.org",
+            AliasRoutes.ROOT_PATH + "/bob@james.org/sources/bob-alias@james.org",
             GlobalQuotaRoutes.QUOTA_ENDPOINT + "/count",
             GlobalQuotaRoutes.QUOTA_ENDPOINT + "/size",
             GroupsRoutes.ROOT_PATH + "/group@james.org/user@james.org",

http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/AliasSourcesResponse.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/AliasSourcesResponse.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/AliasSourcesResponse.java
new file mode 100644
index 0000000..a57988b
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/AliasSourcesResponse.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.webadmin.dto;
+
+public class AliasSourcesResponse {
+    private final String source;
+
+    public AliasSourcesResponse(String source) {
+        this.source = source;
+    }
+
+    public String getSource() {
+        return source;
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
index 0a06aa0..43004d0 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
@@ -22,7 +22,12 @@ package org.apache.james.webadmin.routes;
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 import static spark.Spark.halt;
 
+import java.util.List;
+import java.util.Map;
+
 import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -33,15 +38,22 @@ import org.apache.james.rrt.api.MappingAlreadyExistsException;
 import org.apache.james.rrt.api.RecipientRewriteTable;
 import org.apache.james.rrt.api.RecipientRewriteTableException;
 import org.apache.james.rrt.api.SameSourceAndDestinationException;
+import org.apache.james.rrt.lib.Mapping;
 import org.apache.james.rrt.lib.MappingSource;
+import org.apache.james.rrt.lib.Mappings;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.util.OptionalUtils;
 import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.dto.AliasSourcesResponse;
 import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
 
+import com.github.steveash.guavate.Guavate;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
@@ -70,12 +82,14 @@ public class AliasRoutes implements Routes {
     private static final String ADDRESS_TYPE = "alias";
 
     private final UsersRepository usersRepository;
+    private final JsonTransformer jsonTransformer;
     private final RecipientRewriteTable recipientRewriteTable;
 
     @Inject
     @VisibleForTesting
-    AliasRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository usersRepository) {
+    AliasRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository usersRepository, JsonTransformer jsonTransformer) {
         this.usersRepository = usersRepository;
+        this.jsonTransformer = jsonTransformer;
         this.recipientRewriteTable = recipientRewriteTable;
     }
 
@@ -86,7 +100,29 @@ public class AliasRoutes implements Routes {
 
     @Override
     public void define(Service service) {
-        service.put(USER_IN_ALIAS_SOURCES_ADDRESSES_PATH, this::addToAliasSources);
+        service.get(ROOT_PATH, this::listAddressesWithAliases, jsonTransformer);
+        service.get(ALIAS_ADDRESS_PATH, this::listAliasesOfAddress, jsonTransformer);
+        service.put(USER_IN_ALIAS_SOURCES_ADDRESSES_PATH, this::addAlias);
+        service.delete(USER_IN_ALIAS_SOURCES_ADDRESSES_PATH, this::deleteAlias);
+    }
+
+    @GET
+    @Path(ROOT_PATH)
+    @ApiOperation(value = "getting addresses containing aliases list")
+    @ApiResponses(value = {
+        @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+            message = "Internal server error - Something went bad on the server side.")
+    })
+    public ImmutableSet<String> listAddressesWithAliases(Request request, Response response) throws RecipientRewriteTableException {
+        return recipientRewriteTable.getAllMappings()
+            .entrySet().stream()
+            .filter(e -> e.getValue().contains(Mapping.Type.Alias))
+            .map(Map.Entry::getValue)
+            .flatMap(Mappings::asStream)
+            .flatMap(mapping -> OptionalUtils.toStream(mapping.asMailAddress()))
+            .map(MailAddress::asString)
+            .collect(Guavate.toImmutableSortedSet());
     }
 
     @PUT
@@ -106,10 +142,11 @@ public class AliasRoutes implements Routes {
         @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK"),
         @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = ALIAS_DESTINATION_ADDRESS + " or alias structure format is not valid"),
         @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The alias source exists as an user already"),
+        @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Source and destination can't be the same!"),
         @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
             message = "Internal server error - Something went bad on the server side.")
     })
-    public HaltException addToAliasSources(Request request, Response response) throws UsersRepositoryException, RecipientRewriteTableException {
+    public HaltException addAlias(Request request, Response response) throws UsersRepositoryException, RecipientRewriteTableException {
         MailAddress aliasSourceAddress = MailAddressParser.parseMailAddress(request.params(ALIAS_SOURCE_ADDRESS), ADDRESS_TYPE);
         ensureUserDoesNotExist(aliasSourceAddress);
         MailAddress destinationAddress = MailAddressParser.parseMailAddress(request.params(ALIAS_DESTINATION_ADDRESS), ADDRESS_TYPE);
@@ -143,4 +180,56 @@ public class AliasRoutes implements Routes {
                 .haltError();
         }
     }
+
+    @DELETE
+    @Path(ROOT_PATH + "/{" + ALIAS_DESTINATION_ADDRESS + "}/sources/{" + ALIAS_SOURCE_ADDRESS + "}")
+    @ApiOperation(value = "remove an alias from a destination address")
+    @ApiImplicitParams({
+        @ApiImplicitParam(required = true, dataType = "string", name = ALIAS_DESTINATION_ADDRESS, paramType = "path",
+            value = "Destination mail address of the alias to remove.\n" +
+                MAILADDRESS_ASCII_DISCLAIMER),
+        @ApiImplicitParam(required = true, dataType = "string", name = ALIAS_SOURCE_ADDRESS, paramType = "path",
+            value = "Source mail address of the alias to remove.\n" +
+                MAILADDRESS_ASCII_DISCLAIMER)
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK"),
+        @ApiResponse(code = HttpStatus.BAD_REQUEST_400,
+            message = ALIAS_DESTINATION_ADDRESS + " or alias structure format is not valid"),
+        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+            message = "Internal server error - Something went bad on the server side.")
+    })
+    public HaltException deleteAlias(Request request, Response response) throws RecipientRewriteTableException {
+        MailAddress destinationAddress = MailAddressParser.parseMailAddress(request.params(ALIAS_DESTINATION_ADDRESS), ADDRESS_TYPE);
+        MailAddress aliasToBeRemoved = MailAddressParser.parseMailAddress(request.params(ALIAS_SOURCE_ADDRESS), ADDRESS_TYPE);
+        MappingSource source = MappingSource.fromMailAddress(aliasToBeRemoved);
+        recipientRewriteTable.removeAliasMapping(source, destinationAddress.asString());
+        return halt(HttpStatus.NO_CONTENT_204);
+    }
+
+    @GET
+    @Path(ROOT_PATH + "/{" + ALIAS_DESTINATION_ADDRESS + "}")
+    @ApiOperation(value = "listing alias sources of an address")
+    @ApiImplicitParams({
+        @ApiImplicitParam(required = true, dataType = "string", name = ALIAS_DESTINATION_ADDRESS, paramType = "path")
+    })
+    @ApiResponses(value = {
+        @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+        @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The destination is not an address"),
+        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+            message = "Internal server error - Something went bad on the server side.")
+    })
+    public ImmutableSet<AliasSourcesResponse> listAliasesOfAddress(Request request, Response response) throws RecipientRewriteTableException {
+        MailAddress baseAddress = MailAddressParser.parseMailAddress(request.params(ALIAS_DESTINATION_ADDRESS), ADDRESS_TYPE);
+
+        return recipientRewriteTable.getAllMappings()
+            .entrySet().stream()
+            .filter(e -> e.getValue().contains(Mapping.alias(baseAddress.asString())))
+            .map(Map.Entry::getKey)
+            .flatMap(source -> OptionalUtils.toStream(source.asMailAddress()))
+            .map(MailAddress::asString)
+            .sorted()
+            .map(AliasSourcesResponse::new)
+            .collect(Guavate.toImmutableSet());
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
index 5faad03..af6e4e1 100644
--- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
@@ -31,13 +31,13 @@ import org.apache.james.domainlist.memory.MemoryDomainList;
 import org.apache.james.metrics.logger.DefaultMetricFactory;
 import org.apache.james.rrt.api.RecipientRewriteTable;
 import org.apache.james.rrt.api.RecipientRewriteTableException;
-import org.apache.james.rrt.lib.Mapping;
 import org.apache.james.rrt.lib.MappingSource;
 import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.memory.MemoryUsersRepository;
 import org.apache.james.webadmin.WebAdminServer;
 import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -45,6 +45,7 @@ import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
+import java.util.List;
 import java.util.Map;
 
 import static io.restassured.RestAssured.given;
@@ -53,6 +54,8 @@ import static io.restassured.RestAssured.with;
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doThrow;
@@ -69,15 +72,11 @@ class AliasRoutesTest {
     public static final String BOB_ALIAS_WITH_SLASH = "bob-alias/@" + DOMAIN.name();
     public static final String BOB_ALIAS_WITH_ENCODED_SLASH = "bob-alias%2F@" + DOMAIN.name();
     public static final String ALICE = "alice@" + DOMAIN.name();
+    public static final String ALICE_ALIAS = "alice-alias@" + DOMAIN.name();
     public static final String BOB_PASSWORD = "123456";
     public static final String BOB_WITH_SLASH_PASSWORD = "abcdef";
     public static final String ALICE_PASSWORD = "789123";
 
-    private static final MappingSource BOB_ALIAS_SOURCE = MappingSource.fromUser("bob-alias", DOMAIN);
-    private static final MappingSource BOB_ALIAS_WITH_ENCODED_SLASH_SOURCE = MappingSource.fromUser("bob-alias/", DOMAIN);
-    private static final Mapping BOB_MAPPING = Mapping.alias(BOB);
-    private static final Mapping BOB_WITH_ENCODED_SLASH_MAPPING = Mapping.alias(BOB_WITH_SLASH);
-
     private WebAdminServer webAdminServer;
 
     private void createServer(AliasRoutes aliasRoutes) throws Exception {
@@ -123,7 +122,58 @@ class AliasRoutesTest {
             usersRepository.addUser(BOB_WITH_SLASH, BOB_WITH_SLASH_PASSWORD);
             usersRepository.addUser(ALICE, ALICE_PASSWORD);
 
-            createServer(new AliasRoutes(memoryRecipientRewriteTable, usersRepository));
+            createServer(new AliasRoutes(memoryRecipientRewriteTable, usersRepository, new JsonTransformer()));
+        }
+
+        @Test
+        void getAliasesShouldBeEmpty() {
+            when()
+                .get()
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body(is("[]"));
+        }
+
+        @Test
+        void getAliasesShouldListExistingAliasesInAlphabeticOrder() {
+            with()
+                .put(ALICE + SEPARATOR + "sources" + SEPARATOR + ALICE_ALIAS);
+
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            List<String> addresses =
+                when()
+                    .get()
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .extract()
+                    .body()
+                    .jsonPath()
+                    .getList(".");
+            assertThat(addresses).containsExactly(ALICE, BOB);
+        }
+
+        @Test
+        void getNotRegisteredAliasesShouldReturnEmptyList() {
+            when()
+                .get("unknown@domain.travel")
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body(is("[]"));
+        }
+
+        @Test
+        void getAliasesShouldReturnEmptyListWhenNoAliasMappings() {
+            when()
+                .get(BOB)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body(is("[]"));
         }
 
         @Test
@@ -202,7 +252,12 @@ class AliasRoutesTest {
             with()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
 
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_ALIAS_SOURCE)).containsOnly(BOB_MAPPING);
+            when()
+                .get(BOB)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS));
         }
 
         @Test
@@ -210,15 +265,25 @@ class AliasRoutesTest {
             with()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS_WITH_ENCODED_SLASH);
 
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_ALIAS_WITH_ENCODED_SLASH_SOURCE)).containsOnly(BOB_MAPPING);
+            when()
+                .get(BOB)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS_WITH_SLASH));
         }
 
         @Test
-        void putAliasForUserWithEncodedSlashShouldCreateForward() {
+        void putAliasForUserWithEncodedSlashShouldCreateAlias() {
             with()
                 .put(BOB_WITH_ENCODED_SLASH + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
 
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_ALIAS_SOURCE)).containsOnly(BOB_WITH_ENCODED_SLASH_MAPPING);
+            when()
+                .get(BOB_WITH_ENCODED_SLASH)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS));
         }
 
         @Test
@@ -229,34 +294,106 @@ class AliasRoutesTest {
             with()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
 
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_ALIAS_SOURCE)).containsOnly(BOB_MAPPING);
+            when()
+                .get(BOB)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS));
         }
 
         @Test
-        void putAliasForUserShouldAllowSeveralSources() {
-            MappingSource source2 = MappingSource.fromUser("bob-alias2", DOMAIN);
-
+        void putAliasForUserShouldAllowSeveralSourcesAndReturnThemInAlphabeticalOrder() {
             with()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
 
             with()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS_2);
 
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_ALIAS_SOURCE)).containsOnly(BOB_MAPPING);
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(source2)).containsOnly(BOB_MAPPING);
+            when()
+                .get(BOB)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS, BOB_ALIAS_2));
         }
 
         @Test
         void putAliasForUserShouldAllowSeveralDestinations() {
-            Mapping aliceMapping = Mapping.alias(ALICE);
-
             with()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
 
             with()
                 .put(ALICE + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
 
-            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_ALIAS_SOURCE)).containsOnly(BOB_MAPPING, aliceMapping);
+            when()
+                .get(BOB)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS));
+
+            when()
+                .get(ALICE)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS));
+        }
+
+        @Test
+        void putAliasForUserShouldNotRequireExistingBaseUser() {
+            String notExistingAddress = "notFound@" + DOMAIN.name();
+
+            with()
+                .put(notExistingAddress + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            when()
+                .get(notExistingAddress)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body("source", hasItems(BOB_ALIAS));
+        }
+
+        @Test
+        void deleteAliasNotInAliasesShouldReturnOK() {
+            when()
+                .delete(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.NO_CONTENT_204);
+        }
+
+        @Test
+        void deleteAliasInAliasesShouldDeleteAliasForUser() {
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            with()
+                .delete(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            when()
+                .get(BOB_ALIAS)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body(is("[]"));
+        }
+
+        @Test
+        void deleteLastAliasOfUserInAliasesShouldDeleteUserFromAliasList() {
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            with()
+                .delete(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            when()
+                .get()
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.OK_200)
+                .body(is("[]"));
         }
     }
 
@@ -284,7 +421,7 @@ class AliasRoutesTest {
             UsersRepository userRepository = mock(UsersRepository.class);
             DomainList domainList = mock(DomainList.class);
             Mockito.when(domainList.containsDomain(any())).thenReturn(true);
-            createServer(new AliasRoutes(memoryRecipientRewriteTable, userRepository));
+            createServer(new AliasRoutes(memoryRecipientRewriteTable, userRepository, new JsonTransformer()));
         }
 
         @Test
@@ -326,7 +463,7 @@ class AliasRoutesTest {
         }
 
         @Test
-        void putUserDestinationInForwardWithSlashShouldReturnNotFound() {
+        void putUserDestinationInAliasWithSlashShouldReturnNotFound() {
             when()
                 .put(BOB_WITH_SLASH + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
             .then()
@@ -350,6 +487,52 @@ class AliasRoutesTest {
         }
 
         @Test
+        void deleteMalformedUserDestinationShouldReturnBadRequest() {
+            Map<String, Object> errors = when()
+                .delete("not-an-address" + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+                .containsEntry("type", "InvalidArgument")
+                .containsEntry("message", "The alias is not an email address")
+                .containsEntry("details", "Out of data at position 1 in 'not-an-address'");
+        }
+
+        @Test
+        void deleteMalformedAliasShouldReturnBadRequest() {
+            Map<String, Object> errors = when()
+                .delete(BOB + SEPARATOR + "sources" + SEPARATOR + "not-an-address")
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .contentType(ContentType.JSON)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+                .containsEntry("type", "InvalidArgument")
+                .containsEntry("message", "The alias is not an email address")
+                .containsEntry("details", "Out of data at position 1 in 'not-an-address'");
+        }
+
+        @Test
+        void deleteRequiresTwoPathParams() {
+            when()
+                .delete(BOB)
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404);
+        }
+
+        @Test
         void putShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
             doThrow(RecipientRewriteTableException.class)
                 .when(memoryRecipientRewriteTable)
@@ -372,5 +555,77 @@ class AliasRoutesTest {
             .then()
                 .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
         }
+
+        @Test
+        void getAllShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+            doThrow(RecipientRewriteTableException.class)
+                .when(memoryRecipientRewriteTable)
+                .getAllMappings();
+
+            when()
+                .get()
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+
+        @Test
+        void getAllShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+            doThrow(RuntimeException.class)
+                .when(memoryRecipientRewriteTable)
+                .getAllMappings();
+
+            when()
+                .get()
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+
+        @Test
+        void deleteShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+            doThrow(RecipientRewriteTableException.class)
+                .when(memoryRecipientRewriteTable)
+                .removeAliasMapping(any(), anyString());
+
+            when()
+                .delete(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+
+        @Test
+        void deleteShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+            doThrow(RuntimeException.class)
+                .when(memoryRecipientRewriteTable)
+                .removeAliasMapping(any(), anyString());
+
+            when()
+                .delete(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+
+        @Test
+        void getShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+            doThrow(RecipientRewriteTableException.class)
+                .when(memoryRecipientRewriteTable)
+                .getAllMappings();
+
+            when()
+                .get(BOB)
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+
+        @Test
+        void getShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+            doThrow(RuntimeException.class)
+                .when(memoryRecipientRewriteTable)
+                .getAllMappings();
+
+            when()
+                .get(BOB)
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
     }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/b312b877/src/site/markdown/server/manage-webadmin.md
----------------------------------------------------------------------
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index c0a7b8b..65d28d6 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -1314,7 +1314,46 @@ to be configured.
 
 Note that email addresses are restricted to ASCII character set. Mail addresses not matching this criteria will be rejected.
 
+ - [Listing users with aliases](#Listing_users_with_aliases)
+ - [Listing alias sources of an user](#Listing_alias_sources_of_an_user)
  - [Adding a new alias to an user](#Adding_a_new_alias_to_an_user)
+ - [Removing an alias of an user](#Removing_an_alias_of_an_user)
+
+### Listing users with aliases
+
+```
+curl -XGET http://ip:port/address/aliases
+```
+
+Will return the users having aliases configured as a list of JSON Strings representing mail addresses. For instance:
+
+```
+["user1@domain.com", "user2@domain.com"]
+```
+
+Response codes:
+
+ - 200: Success
+
+### Listing alias sources of an user
+
+```
+curl -XGET http://ip:port/address/aliases/user@domain.com
+```
+
+Will return the aliases of this user as a list of JSON Strings representing mail addresses. For instance:
+
+```
+[
+  {"source":"alias1@domain.com"},
+  {"source":"alias2@domain.com"}
+]
+```
+
+Response codes:
+
+ - 200: Success
+ - 400: Alias structure is not valid
 
 ### Adding a new alias to an user
 
@@ -1329,6 +1368,20 @@ Response codes:
  - 204: OK
  - 400: Alias structure or member is not valid
  - 400: The alias source exists as an user already
+ - 400: Source and destination can't be the same!
+
+### Removing an alias of an user
+
+```
+curl -XDELETE http://ip:port/address/aliases/user@domain.com/sources/alias@domain.com
+```
+
+Will remove alias@domain.com from user@domain.com, removing the alias if needed
+
+Response codes:
+
+ - 204: OK
+ - 400: Alias structure or member is not valid
 
 ## Administrating mail repositories
 


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