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/11 03:01:19 UTC

[09/10] james-project git commit: JAMES-2637 add PUT route for aliases routes

JAMES-2637 add PUT route for aliases 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/de15eaa6
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/de15eaa6
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/de15eaa6

Branch: refs/heads/master
Commit: de15eaa6c4178b39d2405c2c944651456ea8b03a
Parents: 14e264c
Author: Rene Cordier <rc...@linagora.com>
Authored: Tue Jan 8 16:28:14 2019 +0700
Committer: Benoit Tellier <bt...@linagora.com>
Committed: Fri Jan 11 09:48:34 2019 +0700

----------------------------------------------------------------------
 .../james/modules/server/DataRoutesModules.java |   2 +
 .../integration/UnauthorizedEndpointsTest.java  |   2 +
 .../WebAdminServerIntegrationTest.java          |   1 +
 .../james/webadmin/routes/AliasRoutes.java      | 139 ++++++++
 .../webadmin/routes/MailAddressParser.java      |  60 ++++
 .../james/webadmin/routes/AliasRoutesTest.java  | 348 +++++++++++++++++++
 src/site/markdown/server/manage-webadmin.md     |  31 ++
 7 files changed, 583 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
----------------------------------------------------------------------
diff --git a/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java b/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
index ef695fa..a2726af 100644
--- a/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
+++ b/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
@@ -20,6 +20,7 @@
 package org.apache.james.modules.server;
 
 import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.routes.AliasRoutes;
 import org.apache.james.webadmin.routes.DomainMappingsRoutes;
 import org.apache.james.webadmin.routes.DomainsRoutes;
 import org.apache.james.webadmin.routes.ForwardRoutes;
@@ -34,6 +35,7 @@ public class DataRoutesModules extends AbstractModule {
     @Override
     protected void configure() {
         Multibinder<Routes> routesMultibinder = Multibinder.newSetBinder(binder(), Routes.class);
+        routesMultibinder.addBinding().to(AliasRoutes.class);
         routesMultibinder.addBinding().to(DomainsRoutes.class);
         routesMultibinder.addBinding().to(DomainMappingsRoutes.class);
         routesMultibinder.addBinding().to(ForwardRoutes.class);

http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/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 dc2140f..f20b041 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
@@ -24,6 +24,7 @@ import static io.restassured.RestAssured.when;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.utils.WebAdminGuiceProbe;
 import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.routes.AliasRoutes;
 import org.apache.james.webadmin.routes.CassandraMigrationRoutes;
 import org.apache.james.webadmin.routes.DLPConfigurationRoutes;
 import org.apache.james.webadmin.routes.DomainMappingsRoutes;
@@ -127,6 +128,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",
             GlobalQuotaRoutes.QUOTA_ENDPOINT,

http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java b/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
index 61e690b..23c0e2e 100644
--- a/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
+++ b/server/protocols/webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
@@ -326,6 +326,7 @@ public class WebAdminServerIntegrationTest {
             .body(containsString("\"tags\":[\"MailRepositories\"]"))
             .body(containsString("\"tags\":[\"MailQueues\"]"))
             .body(containsString("\"tags\":[\"Address Forwards\"]"))
+            .body(containsString("\"tags\":[\"Address Aliases\"]"))
             .body(containsString("\"tags\":[\"Address Groups\"]"))
             .body(containsString("{\"name\":\"ReIndexing (mailboxes)\"}"))
             .body(containsString("{\"name\":\"MessageIdReIndexing\"}"));

http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/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
new file mode 100644
index 0000000..583858d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
@@ -0,0 +1,139 @@
+/****************************************************************
+ * 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.routes;
+
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static spark.Spark.halt;
+
+import javax.inject.Inject;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.User;
+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.lib.MappingSource;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.webadmin.Constants;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.eclipse.jetty.http.HttpStatus;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import spark.HaltException;
+import spark.Request;
+import spark.Response;
+import spark.Service;
+
+@Api(tags = "Address Aliases")
+@Path(AliasRoutes.ROOT_PATH)
+@Produces(Constants.JSON_CONTENT_TYPE)
+public class AliasRoutes implements Routes {
+
+    public static final String ROOT_PATH = "address/aliases";
+
+    private static final String ALIAS_DESTINATION_ADDRESS = "aliasDestinationAddress";
+    private static final String ALIAS_ADDRESS_PATH = ROOT_PATH + SEPARATOR + ":" + ALIAS_DESTINATION_ADDRESS;
+    private static final String ALIAS_SOURCE_ADDRESS = "aliasSourceAddress";
+    private static final String USER_IN_ALIAS_SOURCES_ADDRESSES_PATH = ALIAS_ADDRESS_PATH + SEPARATOR +
+        "sources" + SEPARATOR + ":" + ALIAS_SOURCE_ADDRESS;
+    private static final String MAILADDRESS_ASCII_DISCLAIMER = "Note that email addresses are restricted to ASCII character set. " +
+        "Mail addresses not matching this criteria will be rejected.";
+    private static final String ADDRESS_TYPE = "alias";
+
+    private final UsersRepository usersRepository;
+    private final RecipientRewriteTable recipientRewriteTable;
+
+    @Inject
+    @VisibleForTesting
+    AliasRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository usersRepository) {
+        this.usersRepository = usersRepository;
+        this.recipientRewriteTable = recipientRewriteTable;
+    }
+
+    @Override
+    public String getBasePath() {
+        return ROOT_PATH;
+    }
+
+    @Override
+    public void define(Service service) {
+        service.put(USER_IN_ALIAS_SOURCES_ADDRESSES_PATH, this::addToAliasSources);
+    }
+
+    @PUT
+    @Path(ROOT_PATH + "/{" + ALIAS_DESTINATION_ADDRESS + "}/sources/{" + ALIAS_SOURCE_ADDRESS + "}")
+    @ApiOperation(value = "adding a source address into an alias")
+    @ApiImplicitParams({
+        @ApiImplicitParam(required = true, dataType = "string", name = ALIAS_DESTINATION_ADDRESS, paramType = "path",
+            value = "Destination mail address of the alias. Sending a mail to the alias source address will send it to " +
+                "that email address.\n" +
+                MAILADDRESS_ASCII_DISCLAIMER),
+        @ApiImplicitParam(required = true, dataType = "string", name = ALIAS_SOURCE_ADDRESS, paramType = "path",
+            value = "Source mail address of the alias. Sending a mail to that address will send it to " +
+                "the email destination address.\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.BAD_REQUEST_400, message = "The alias source exists as an user already"),
+        @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 {
+        MailAddress aliasSourceAddress = MailAddressParser.parseMailAddress(request.params(ALIAS_SOURCE_ADDRESS), ADDRESS_TYPE);
+        ensureUserDoesNotExist(aliasSourceAddress);
+        MailAddress destinationAddress = MailAddressParser.parseMailAddress(request.params(ALIAS_DESTINATION_ADDRESS), ADDRESS_TYPE);
+        MappingSource source = MappingSource.fromUser(User.fromMailAddress(destinationAddress));
+        addAlias(source, aliasSourceAddress);
+        return halt(HttpStatus.NO_CONTENT_204);
+    }
+
+    private void addAlias(MappingSource source, MailAddress aliasSourceAddress) throws RecipientRewriteTableException {
+        try {
+            recipientRewriteTable.addAliasMapping(source, aliasSourceAddress.asString());
+        } catch (MappingAlreadyExistsException e) {
+            // ignore
+        }
+    }
+
+    private void ensureUserDoesNotExist(MailAddress mailAddress) throws UsersRepositoryException {
+        String username = usersRepository.getUser(mailAddress);
+
+        if (usersRepository.contains(username)) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("The alias source exists as an user already")
+                .haltError();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MailAddressParser.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MailAddressParser.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MailAddressParser.java
new file mode 100644
index 0000000..ff363a0
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MailAddressParser.java
@@ -0,0 +1,60 @@
+/****************************************************************
+ * 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.routes;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import javax.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.eclipse.jetty.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class MailAddressParser {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(MailAddressParser.class);
+
+    static MailAddress parseMailAddress(String address, String addressType) {
+        try {
+            String decodedAddress = URLDecoder.decode(address, StandardCharsets.UTF_8.displayName());
+            return new MailAddress(decodedAddress);
+        } catch (AddressException e) {
+            LOGGER.error("The " + addressType + " " + address + " is not an email address");
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("The " + addressType + " is not an email address")
+                .cause(e)
+                .haltError();
+        } catch (UnsupportedEncodingException e) {
+            LOGGER.error("UTF-8 should be a valid encoding");
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+                .type(ErrorResponder.ErrorType.SERVER_ERROR)
+                .message("Internal server error - Something went bad on the server side.")
+                .cause(e)
+                .haltError();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/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
new file mode 100644
index 0000000..9eb0744
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
@@ -0,0 +1,348 @@
+/****************************************************************
+ * 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.routes;
+
+import io.restassured.RestAssured;
+import io.restassured.filter.log.LogDetail;
+import io.restassured.http.ContentType;
+import org.apache.commons.configuration.DefaultConfigurationBuilder;
+import org.apache.james.core.Domain;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.domainlist.lib.DomainListConfiguration;
+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.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Map;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+class AliasRoutesTest {
+
+    private static final Domain DOMAIN = Domain.of("b.com");
+    public static final String BOB = "bob@" + DOMAIN.name();
+    public static final String BOB_WITH_SLASH = "bob/@" + DOMAIN.name();
+    public static final String BOB_WITH_ENCODED_SLASH = "bob%2F@" + DOMAIN.name();
+    public static final String BOB_ALIAS = "bob-alias@" + DOMAIN.name();
+    public static final String BOB_ALIAS_2 = "bob-alias2@" + DOMAIN.name();
+    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 BOB_PASSWORD = "123456";
+    public static final String BOB_WITH_SLASH_PASSWORD = "abcdef";
+    public static final String ALICE_PASSWORD = "789123";
+
+    private static final MappingSource BOB_SOURCE = MappingSource.fromUser("bob", DOMAIN);
+    private static final MappingSource BOB_WITH_ENCODED_SLASH_SOURCE = MappingSource.fromUser("bob/", DOMAIN);
+    private static final Mapping BOB_MAPPING = Mapping.alias(BOB_ALIAS);
+
+    private WebAdminServer webAdminServer;
+
+    private void createServer(AliasRoutes aliasRoutes) throws Exception {
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+            new DefaultMetricFactory(),
+            aliasRoutes);
+        webAdminServer.configure(NO_CONFIGURATION);
+        webAdminServer.await();
+
+        RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath("address/aliases")
+            .log(LogDetail.METHOD)
+            .build();
+    }
+
+    @AfterEach
+    void stop() {
+        webAdminServer.destroy();
+    }
+
+    @Nested
+    class NormalBehaviour {
+
+        MemoryUsersRepository usersRepository;
+        MemoryDomainList domainList;
+        MemoryRecipientRewriteTable memoryRecipientRewriteTable;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            memoryRecipientRewriteTable = new MemoryRecipientRewriteTable();
+            DNSService dnsService = mock(DNSService.class);
+            domainList = new MemoryDomainList(dnsService);
+            domainList.configure(DomainListConfiguration.builder()
+                .autoDetect(false)
+                .autoDetectIp(false));
+            domainList.addDomain(DOMAIN);
+
+            usersRepository = MemoryUsersRepository.withVirtualHosting();
+            usersRepository.setDomainList(domainList);
+            usersRepository.configure(new DefaultConfigurationBuilder());
+
+            usersRepository.addUser(BOB, BOB_PASSWORD);
+            usersRepository.addUser(BOB_WITH_SLASH, BOB_WITH_SLASH_PASSWORD);
+            usersRepository.addUser(ALICE, ALICE_PASSWORD);
+
+            createServer(new AliasRoutes(memoryRecipientRewriteTable, usersRepository));
+        }
+
+        @Test
+        void putAliasForUserShouldReturnNoContent() {
+            when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.NO_CONTENT_204);
+        }
+
+        @Test
+        void putAliasShouldBeIdempotent() {
+            given()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.NO_CONTENT_204);
+        }
+
+        @Test
+        void putAliasWithSlashForUserShouldReturnNoContent() {
+            when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS_WITH_ENCODED_SLASH)
+            .then()
+                .statusCode(HttpStatus.NO_CONTENT_204);
+        }
+
+        @Test
+        void putUserForAliasWithEncodedSlashShouldReturnNoContent() {
+            when()
+                .put(BOB_WITH_ENCODED_SLASH + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.NO_CONTENT_204);
+        }
+
+        @Test
+        void putExistingUserAsAliasSourceShouldNotBePossible() {
+            Map<String, Object> errors = when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + ALICE)
+            .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 source exists as an user already");
+        }
+
+        @Test
+        void putAliasForUserShouldCreateAlias() {
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_SOURCE)).containsOnly(BOB_MAPPING);
+        }
+
+        @Test
+        void putAliasWithEncodedSlashForUserShouldAddItAsADestination() {
+            Mapping mapping = Mapping.alias(BOB_ALIAS_WITH_SLASH);
+
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS_WITH_ENCODED_SLASH);
+
+            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_SOURCE)).containsOnly(mapping);
+        }
+
+        @Test
+        void putAliasForUserWithEncodedSlashShouldCreateForward() {
+            with()
+                .put(BOB_WITH_ENCODED_SLASH + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_WITH_ENCODED_SLASH_SOURCE)).containsOnly(BOB_MAPPING);
+        }
+
+        @Test
+        void putSameAliasForUserTwiceShouldBeIdempotent() {
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_SOURCE)).containsOnly(BOB_MAPPING);
+        }
+
+        @Test
+        void putAliasForUserShouldAllowSeveralSources() {
+            Mapping mapping2 = Mapping.alias(BOB_ALIAS_2);
+
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS);
+
+            with()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS_2);
+
+            assertThat(memoryRecipientRewriteTable.getStoredMappings(BOB_SOURCE)).containsOnly(BOB_MAPPING, mapping2);
+        }
+    }
+
+    @Nested
+    class FilteringOtherRewriteRuleTypes extends NormalBehaviour {
+
+        @BeforeEach
+        void setup() throws Exception {
+            super.setUp();
+            memoryRecipientRewriteTable.addErrorMapping(MappingSource.fromUser("error", DOMAIN), "disabled");
+            memoryRecipientRewriteTable.addRegexMapping(MappingSource.fromUser("regex", DOMAIN), ".*@b\\.com");
+            memoryRecipientRewriteTable.addAliasDomainMapping(MappingSource.fromDomain(Domain.of("alias")), DOMAIN);
+        }
+
+    }
+
+    @Nested
+    class ExceptionHandling {
+
+        private RecipientRewriteTable memoryRecipientRewriteTable;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            memoryRecipientRewriteTable = mock(RecipientRewriteTable.class);
+            UsersRepository userRepository = mock(UsersRepository.class);
+            DomainList domainList = mock(DomainList.class);
+            Mockito.when(domainList.containsDomain(any())).thenReturn(true);
+            createServer(new AliasRoutes(memoryRecipientRewriteTable, userRepository));
+        }
+
+        @Test
+        void putMalformedUserDestinationShouldReturnBadRequest() {
+            Map<String, Object> errors = when()
+                .put("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 putMalformedAliasSourceShouldReturnBadRequest() {
+            Map<String, Object> errors = when()
+                .put(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 putUserDestinationInForwardWithSlashShouldReturnNotFound() {
+            when()
+                .put(BOB_WITH_SLASH + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404);
+        }
+
+        @Test
+        void putAliasSourceWithSlashShouldReturnNotFound() {
+            when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS_WITH_SLASH)
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404);
+        }
+
+        @Test
+        void putRequiresTwoPathParams() {
+            when()
+                .put(BOB)
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404);
+        }
+
+        @Test
+        void putShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+            doThrow(RecipientRewriteTableException.class)
+                .when(memoryRecipientRewriteTable)
+                .addAliasMapping(any(), anyString());
+
+            when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+
+        @Test
+        void putShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+            doThrow(RuntimeException.class)
+                .when(memoryRecipientRewriteTable)
+                .addAliasMapping(any(), anyString());
+
+            when()
+                .put(BOB + SEPARATOR + "sources" + SEPARATOR + BOB_ALIAS)
+            .then()
+                .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/de15eaa6/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 2ba0255..c0a7b8b 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -37,6 +37,7 @@ as exposed above). To avoid information duplication, this is ommited on endpoint
  - [Correcting ghost mailbox](#Correcting_ghost_mailbox)
  - [Creating address group](#Creating_address_group)
  - [Creating address forwards](#Creating_address_forwards)
+ - [Creating address aliases](#Creating_address_aliases)
  - [Administrating mail repositories](#Administrating_mail_repositories)
  - [Administrating mail queues](#Administrating_mail_queues)
  - [Administrating DLP Configuration](#Administrating_dlp_configuration)
@@ -1299,6 +1300,36 @@ Response codes:
  - 204: Success
  - 400: Forward structure or member is not valid
 
+## Creating address aliases
+
+You can use **webadmin** to define aliases for an user.
+
+When a specific email is sent to the alias address, the destination address of the alias will receive it.
+
+Aliases can be defined for existing users.
+
+This feature uses [Recipients rewrite table](/server/config-recipientrewritetable.html) and requires
+the [RecipientRewriteTable mailet](https://github.com/apache/james-project/blob/master/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/RecipientRewriteTable.java)
+to be configured.
+
+Note that email addresses are restricted to ASCII character set. Mail addresses not matching this criteria will be rejected.
+
+ - [Adding a new alias to an user](#Adding_a_new_alias_to_an_user)
+
+### Adding a new alias to an user
+
+```
+curl -XPUT http://ip:port/address/aliases/user@domain.com/sources/alias@domain.com
+```
+
+Will add alias@domain.com to user@domain.com, creating the alias if needed
+
+Response codes:
+
+ - 204: OK
+ - 400: Alias structure or member is not valid
+ - 400: The alias source exists as an user already
+
 ## Administrating mail repositories
 
  - [Create a mail repository](#Create_a_mail_repository)


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