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 <rcord...@linagora.com> Authored: Tue Jan 8 16:28:14 2019 +0700 Committer: Benoit Tellier <btell...@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 + "/j...@perdu.com/size", UserRoutes.USERS + "/u...@james.org", ForwardRoutes.ROOT_PATH + "/al...@james.org/b...@james.org", + AliasRoutes.ROOT_PATH + "/b...@james.org/sources/bob-al...@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/u...@domain.com/sources/al...@domain.com +``` + +Will add al...@domain.com to u...@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-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org