blob: 316e8e4e0466292c28c6aacc90d49c4c80e27383 [file] [log] [blame]
/****************************************************************
* 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.rrt.lib;
import static org.apache.james.UserEntityValidator.EntityType.ALIAS;
import static org.apache.james.UserEntityValidator.EntityType.GROUP;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Stream;
import javax.inject.Inject;
import jakarta.mail.internet.AddressException;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.james.RecipientRewriteTableUserEntityValidator;
import org.apache.james.UserEntityValidator;
import org.apache.james.core.Domain;
import org.apache.james.core.MailAddress;
import org.apache.james.core.Username;
import org.apache.james.domainlist.api.DomainList;
import org.apache.james.domainlist.api.DomainListException;
import org.apache.james.lifecycle.api.Configurable;
import org.apache.james.rrt.api.InvalidRegexException;
import org.apache.james.rrt.api.LoopDetectedException;
import org.apache.james.rrt.api.MappingAlreadyExistsException;
import org.apache.james.rrt.api.MappingConflictException;
import org.apache.james.rrt.api.RecipientRewriteTable;
import org.apache.james.rrt.api.RecipientRewriteTableConfiguration;
import org.apache.james.rrt.api.RecipientRewriteTableException;
import org.apache.james.rrt.api.SameSourceAndDestinationException;
import org.apache.james.rrt.api.SourceDomainIsNotInDomainListException;
import org.apache.james.rrt.lib.Mapping.Type;
import org.apache.james.user.api.UsersRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.fge.lambdas.Throwing;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
public abstract class AbstractRecipientRewriteTable implements RecipientRewriteTable, Configurable {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRecipientRewriteTable.class);
private RecipientRewriteTableConfiguration configuration;
private UserEntityValidator userEntityValidator;
private UsersRepository usersRepository;
private DomainList domainList;
public void setConfiguration(RecipientRewriteTableConfiguration configuration) {
Preconditions.checkState(this.configuration == null, "A configuration cannot be set twice");
this.configuration = configuration;
this.userEntityValidator = new RecipientRewriteTableUserEntityValidator(this);
}
@Inject
public void setUsersRepository(UsersRepository usersRepository) {
this.usersRepository = usersRepository;
}
@Inject
public void setUserEntityValidator(UserEntityValidator userEntityValidator) {
this.userEntityValidator = userEntityValidator;
}
@Inject
public void setDomainList(DomainList domainList) {
this.domainList = domainList;
}
@Override
public void configure(HierarchicalConfiguration<ImmutableNode> config) throws ConfigurationException {
setConfiguration(RecipientRewriteTableConfiguration.fromConfiguration(config));
doConfigure(config);
}
protected void doConfigure(HierarchicalConfiguration<ImmutableNode> arg0) throws ConfigurationException {
}
@Override
public RecipientRewriteTableConfiguration getConfiguration() {
return configuration;
}
@Override
public Mappings getResolvedMappings(String user, Domain domain, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
Preconditions.checkState(this.configuration != null, "RecipientRewriteTable is not configured");
return asUsername(user, domain)
.map(Throwing.<Username, Mappings>function(username -> getMappings(username, configuration.getMappingLimit(), mappingTypes)).sneakyThrow())
.orElse(MappingsImpl.empty());
}
private static Optional<Username> asUsername(String user, Domain domain) {
try {
return Optional.of(Username.fromLocalPartWithDomain(user, domain));
} catch (IllegalArgumentException e) {
LOGGER.info("Valid mail address {}@{} could not be converted into a username. Assuming empty mappigns.", user, domain.asString(), e);
return Optional.empty();
}
}
private Mappings getMappings(Username username, int mappingLimit, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
// We have to much mappings throw ErrorMappingException to avoid
// infinity loop
if (mappingLimit == 0) {
throw new TooManyMappingException("554 Too many mappings to process");
}
Domain domain = username.getDomainPart().get();
String localPart = username.getLocalPart();
Stream<Mapping> targetMappings = mapAddress(localPart, domain).asStream()
.filter(mapping -> mappingTypes.contains(mapping.getType()));
try {
return MappingsImpl.fromMappings(
targetMappings
.flatMap(Throwing.function((Mapping target) -> convertAndRecurseMapping(username, target, mappingLimit, mappingTypes)).sneakyThrow()));
} catch (SkipMappingProcessingException e) {
return MappingsImpl.empty();
}
}
private Stream<Mapping> convertAndRecurseMapping(Username originalUsername, Mapping associatedMapping, int remainingLoops, EnumSet<Type> mappingTypes) throws ErrorMappingException, SkipMappingProcessingException, AddressException {
Function<Username, Stream<Mapping>> convertAndRecurseMapping =
Throwing
.function((Username rewrittenUser) -> convertAndRecurseMapping(associatedMapping, originalUsername, rewrittenUser, remainingLoops, mappingTypes))
.sneakyThrow();
return associatedMapping.rewriteUser(originalUsername)
.map(rewrittenUser -> rewrittenUser.withDefaultDomainFromUser(originalUsername))
.map(convertAndRecurseMapping)
.orElse(Stream.empty());
}
private Stream<Mapping> convertAndRecurseMapping(Mapping mapping, Username originalUsername, Username rewrittenUsername, int remainingLoops, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
LOGGER.debug("Valid virtual user mapping {} to {}", originalUsername.asString(), rewrittenUsername.asString());
Stream<Mapping> nonRecursiveResult = Stream.of(toMapping(rewrittenUsername, mapping.getType()));
if (!configuration.isRecursive()) {
return nonRecursiveResult;
}
// Check if the returned mapping is the same as the input. If so we need to handle identity to avoid loops.
if (originalUsername.equals(rewrittenUsername)) {
return mapping.handleIdentity(nonRecursiveResult);
} else {
return recurseMapping(nonRecursiveResult, rewrittenUsername, remainingLoops, mappingTypes);
}
}
private Stream<Mapping> recurseMapping(Stream<Mapping> nonRecursiveResult, Username targetUsername, int remainingLoops, EnumSet<Type> mappingTypes) throws ErrorMappingException, RecipientRewriteTableException {
Mappings childMappings = getMappings(targetUsername, remainingLoops - 1, mappingTypes);
if (childMappings.isEmpty()) {
return nonRecursiveResult;
} else {
return childMappings.asStream();
}
}
private Mapping toMapping(Username rewrittenUsername, Type type) {
switch (type) {
case Forward:
case Group:
case Alias:
return Mapping.of(type, rewrittenUsername.asString());
case Regex:
case Domain:
case DomainAlias:
case Error:
case Address:
return Mapping.address(rewrittenUsername.asString());
}
throw new IllegalArgumentException("unhandled enum type");
}
@Override
public void addRegexMapping(MappingSource source, String regex) throws RecipientRewriteTableException {
try {
Pattern.compile(regex);
} catch (PatternSyntaxException e) {
throw new InvalidRegexException("Invalid regex: " + regex, e);
}
Mapping mapping = Mapping.regex(regex);
checkDuplicateMapping(source, mapping);
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add regex mapping => {} for source {}", regex, source.asString());
addMapping(source, mapping);
}
@Override
public void removeRegexMapping(MappingSource source, String regex) throws RecipientRewriteTableException {
LOGGER.info("Remove regex mapping => {} for source: {}", regex, source.asString());
removeMapping(source, Mapping.regex(regex));
}
@Override
public void addAddressMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.address(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
checkHasValidAddress(mapping);
checkDuplicateMapping(source, mapping);
checkDomainMappingSourceIsManaged(source);
checkNotSameSourceAndDestination(source, address);
LOGGER.info("Add address mapping => {} for source: {}", mapping.asString(), source.asString());
assertNoLoop(source, mapping);
addMapping(source, mapping);
}
private Domain defaultDomain() throws RecipientRewriteTableException {
try {
return domainList.getDefaultDomain();
} catch (DomainListException e) {
throw new RecipientRewriteTableException("Unable to retrieve default domain", e);
}
}
private void checkHasValidAddress(Mapping mapping) throws RecipientRewriteTableException {
if (!mapping.asMailAddress().isPresent()) {
throw new RecipientRewriteTableException("Invalid emailAddress: " + mapping.asString());
}
}
@Override
public void removeAddressMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.address(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
LOGGER.info("Remove address mapping => {} for source: {}", mapping.asString(), source.asString());
removeMapping(source, mapping);
}
@Override
public void addErrorMapping(MappingSource source, String error) throws RecipientRewriteTableException {
Mapping mapping = Mapping.error(error);
checkDuplicateMapping(source, mapping);
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add error mapping => {} for source: {}", error, source.asString());
addMapping(source, mapping);
}
@Override
public void removeErrorMapping(MappingSource source, String error) throws RecipientRewriteTableException {
LOGGER.info("Remove error mapping => {} for source: {}", error, source.asString());
removeMapping(source, Mapping.error(error));
}
@Override
public void addDomainMapping(MappingSource source, Domain realDomain) throws RecipientRewriteTableException {
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add domain mapping: {} => {}", source.asDomain().map(Domain::asString).orElse("null"), realDomain);
addMapping(source, Mapping.domain(realDomain));
}
@Override
public void removeDomainMapping(MappingSource source, Domain realDomain) throws RecipientRewriteTableException {
LOGGER.info("Remove domain mapping: {} => {}", source.asDomain().map(Domain::asString).orElse("null"), realDomain);
removeMapping(source, Mapping.domain(realDomain));
}
@Override
public void addDomainAliasMapping(MappingSource source, Domain realDomain) throws RecipientRewriteTableException {
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add domain alias mapping: {} => {}", source.asDomain().map(Domain::asString).orElse("null"), realDomain);
addMapping(source, Mapping.domainAlias(realDomain));
}
@Override
public void removeDomainAliasMapping(MappingSource source, Domain realDomain) throws RecipientRewriteTableException {
LOGGER.info("Remove domain alias mapping: {} => {}", source.asDomain().map(Domain::asString).orElse("null"), realDomain);
removeMapping(source, Mapping.domainAlias(realDomain));
}
@Override
public void addForwardMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.forward(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
checkHasValidAddress(mapping);
checkDuplicateMapping(source, mapping);
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add forward mapping => {} for source: {}", mapping.asString(), source.asString());
assertNoLoop(source, mapping);
addMapping(source, mapping);
}
@Override
public void removeForwardMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.forward(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
LOGGER.info("Remove forward mapping => {} for source: {}", mapping.asString(), source.asString());
removeMapping(source, mapping);
}
@Override
public void addGroupMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.group(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
checkHasValidAddress(mapping);
source.asMailAddress()
.ifPresent(Throwing.consumer(this::ensureGroupNotShadowingAnotherAddress).sneakyThrow());
checkDuplicateMapping(source, mapping);
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add group mapping => {} for source: {}", mapping.asString(), source.asString());
assertNoLoop(source, mapping);
addMapping(source, mapping);
}
private void ensureGroupNotShadowingAnotherAddress(MailAddress groupAddress) throws Exception {
ensureNoConflict(GROUP, groupAddress);
}
private void ensureAliasNotShadowingAnotherAddress(MailAddress groupAddress) throws Exception {
ensureNoConflict(ALIAS, groupAddress);
}
private void ensureNoConflict(UserEntityValidator.EntityType entity, MailAddress groupAddress) throws Exception {
Username username = usersRepository.getUsername(groupAddress);
Optional<UserEntityValidator.ValidationFailure> validationFailure = userEntityValidator.canCreate(username, ImmutableSet.of(entity));
if (validationFailure.isPresent()) {
throw new MappingConflictException(validationFailure.get().errorMessage());
}
}
@Override
public void removeGroupMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.group(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
LOGGER.info("Remove group mapping => {} for source: {}", mapping.asString(), source.asString());
removeMapping(source, mapping);
}
@Override
public void addAliasMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.alias(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
checkHasValidAddress(mapping);
checkDuplicateMapping(source, mapping);
source.asMailAddress()
.ifPresent(Throwing.consumer(this::ensureAliasNotShadowingAnotherAddress).sneakyThrow());
checkNotSameSourceAndDestination(source, address);
checkDomainMappingSourceIsManaged(source);
LOGGER.info("Add alias source => {} for destination mapping: {}", source.asString(), mapping.asString());
assertNoLoop(source, mapping);
addMapping(source, mapping);
}
@Override
public void removeAliasMapping(MappingSource source, String address) throws RecipientRewriteTableException {
Mapping mapping = Mapping.alias(address)
.appendDomainFromThrowingSupplierIfNone(this::defaultDomain);
LOGGER.info("Remove alias source => {} for destination mapping: {}", source.asString(), mapping.asString());
removeMapping(source, mapping);
}
/**
* Return a Map which holds all Mappings
*
* @return Map
*/
public abstract Map<MappingSource, Mappings> getAllMappings() throws RecipientRewriteTableException;
/**
* This method must return stored Mappings for the given user.
* It must never return null but throw RecipientRewriteTableException on errors and return an empty Mappings
* object if no mapping is found.
*/
protected abstract Mappings mapAddress(String user, Domain domain) throws RecipientRewriteTableException;
private void checkDomainMappingSourceIsManaged(MappingSource source) throws RecipientRewriteTableException {
Optional<Domain> notManagedSourceDomain = source.availableDomain()
.filter(Throwing.<Domain>predicate(domain -> !isManagedByDomainList(domain)).sneakyThrow());
if (notManagedSourceDomain.isPresent()) {
throw new SourceDomainIsNotInDomainListException("Source domain '" + notManagedSourceDomain.get().asString() + "' is not managed by the domainList");
}
}
private boolean isManagedByDomainList(Domain domain) throws RecipientRewriteTableException {
try {
return domainList.containsDomain(domain);
} catch (DomainListException e) {
throw new RecipientRewriteTableException("Cannot verify domainList contains the available domain in source");
}
}
private void checkDuplicateMapping(MappingSource source, Mapping mapping) throws RecipientRewriteTableException {
Mappings mappings = getStoredMappings(source);
if (mappings.contains(mapping)) {
throw new MappingAlreadyExistsException("Mapping " + mapping.asString() + " for " + source.asString() + " already exist!");
}
}
private void checkNotSameSourceAndDestination(MappingSource source, String address) throws RecipientRewriteTableException {
if (source.asMailAddress().map(mailAddress -> mailAddress.asString().equals(address)).orElse(false)) {
throw new SameSourceAndDestinationException("Source and destination can't be the same!");
}
}
private void assertNoLoop(MappingSource source, Mapping mapping) throws RecipientRewriteTableException {
if (configuration.isRecursive()) {
boolean leadsToALoop = mapping.asMailAddress()
.map(Throwing.<MailAddress, Mappings>function(
mailAddress -> getResolvedMappings(mailAddress.getLocalPart(), mailAddress.getDomain()))
.sneakyThrow())
.map(mappings -> mappings.asStream()
.flatMap(aMapping -> aMapping.asMailAddress().stream())
.anyMatch(address -> source.asMailAddress().map(address::equals).orElse(false)))
.orElse(false);
if (leadsToALoop) {
throw new LoopDetectedException(source, mapping);
}
}
}
}