blob: 131d2788d898056c39ba957486b1eeb0a613bdf0 [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.domainlist.lib;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.james.core.Domain;
import org.apache.james.dnsservice.api.DNSService;
import org.apache.james.domainlist.api.AutoDetectedDomainRemovalException;
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.util.ReactorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.fge.lambdas.Throwing;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import reactor.core.publisher.Mono;
/**
* All implementations of the DomainList interface should extends this abstract
* class
*/
public abstract class AbstractDomainList implements DomainList, Configurable {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDomainList.class);
enum DomainType {
DefaultDomain,
Internal,
Detected,
DetectedIp
}
public static final String ENV_DOMAIN = "DOMAIN";
private final DNSService dns;
private final EnvDetector envDetector;
private AsyncCache<Domain, Boolean> cache;
private DomainListConfiguration configuration;
private Domain defaultDomain;
public AbstractDomainList(DNSService dns, EnvDetector envDetector) {
this.dns = dns;
this.envDetector = envDetector;
}
public AbstractDomainList(DNSService dns) {
this(dns, new EnvDetector());
}
@Override
public void configure(HierarchicalConfiguration<ImmutableNode> config) throws ConfigurationException {
DomainListConfiguration domainListConfiguration = DomainListConfiguration.from(config);
configure(domainListConfiguration);
}
public void configure(DomainListConfiguration domainListConfiguration) throws ConfigurationException {
this.configuration = domainListConfiguration;
cache = Caffeine.newBuilder()
.expireAfterWrite(configuration.getCacheExpiracy())
.buildAsync();
configureDefaultDomain(domainListConfiguration.getDefaultDomain());
addEnvDomain();
addConfiguredDomains(domainListConfiguration.getConfiguredDomains());
}
public void configure(DomainListConfiguration.Builder configurationBuilder) throws ConfigurationException {
configure(configurationBuilder.build());
}
protected void addConfiguredDomains(List<Domain> domains) {
domains.stream()
.filter(Throwing.predicate((Domain domain) -> !containsDomainInternal(domain)).sneakyThrow())
.forEach(Throwing.consumer(this::addDomain).sneakyThrow());
}
private void addEnvDomain() {
String envDomain = envDetector.getEnv(ENV_DOMAIN);
if (!Strings.isNullOrEmpty(envDomain)) {
try {
LOGGER.info("Adding environment defined domain {}", envDomain);
Domain domain = Domain.of(envDomain);
if (!containsDomain(domain)) {
addDomain(domain);
}
} catch (DomainListException e) {
throw new RuntimeException(e);
}
}
}
@VisibleForTesting void configureDefaultDomain(Domain defaultDomain) throws ConfigurationException {
try {
setDefaultDomain(defaultDomain);
String hostName = InetAddress.getLocalHost().getHostName();
if (mayChangeDefaultDomain()) {
setDefaultDomain(Domain.of(hostName));
}
} catch (UnknownHostException e) {
LOGGER.warn("Unable to retrieve hostname.", e);
} catch (DomainListException e) {
LOGGER.error("An error occured while creating the default domain", e);
}
}
private boolean mayChangeDefaultDomain() {
return configuration.isAutoDetect() && Domain.LOCALHOST.equals(defaultDomain);
}
private void setDefaultDomain(Domain defaultDomain) throws DomainListException {
if (defaultDomain != null && !containsDomain(defaultDomain)) {
addDomain(defaultDomain);
}
this.defaultDomain = defaultDomain;
}
@Override
public Domain getDefaultDomain() throws DomainListException {
if (defaultDomain != null) {
return defaultDomain;
} else {
throw new DomainListException("Null default domain. Domain list might not be configured yet.");
}
}
@Override
public boolean containsDomain(Domain domain) throws DomainListException {
if (configuration.isCacheEnabled()) {
try {
return cache.get(domain, (key, executor) -> innerContains(domain).toFuture()).get();
} catch (ExecutionException e) {
if (e.getCause() instanceof DomainListException) {
throw (DomainListException) e.getCause();
}
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
} else {
return innerContains(domain).block();
}
}
@Override
public Mono<Boolean> containsDomainReactive(Domain domain) {
if (configuration.isCacheEnabled()) {
return Mono.fromFuture(cache.get(domain, (key, executor) -> innerContains(domain).toFuture()));
} else {
return innerContains(domain);
}
}
private Mono<Boolean> innerContains(Domain domain) {
return containsDomainInternalReactive(domain)
.flatMap(containsInternal -> {
if (containsInternal) {
return Mono.just(true);
}
return detectedDomainsContains(domain);
});
}
private Mono<Boolean> detectedDomainsContains(Domain domain) {
if (configuration.isAutoDetect() || configuration.isAutoDetectIp()) {
return Mono.fromCallable(() -> getDomains().contains(domain))
.subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER);
}
return Mono.just(false);
}
@Override
public ImmutableList<Domain> getDomains() throws DomainListException {
Multimap<DomainType, Domain> domainsWithType = getDomainsWithType();
ImmutableSet<Domain> allDomains = domainsWithType.values()
.stream()
.collect(ImmutableSet.toImmutableSet());
if (configuration.isCacheEnabled()) {
domainsWithType.get(DomainType.Internal)
.forEach(domain -> cache.put(domain, CompletableFuture.completedFuture(true)));
}
if (LOGGER.isDebugEnabled()) {
for (Domain domain : allDomains) {
LOGGER.debug("Handling mail for: " + domain.name());
}
}
return ImmutableList.copyOf(allDomains);
}
private Multimap<DomainType, Domain> getDomainsWithType() throws DomainListException {
List<Domain> domains = getDomainListInternal();
ImmutableList<Domain> detectedDomains = detectDomains();
ImmutableList<Domain> domainsWithoutIp = ImmutableList.<Domain>builder()
.addAll(domains)
.addAll(detectedDomains)
.build();
ImmutableList<Domain> ips = detectIps(domainsWithoutIp);
ImmutableMultimap.Builder<DomainType, Domain> result = ImmutableMultimap.<DomainType, Domain>builder()
.putAll(DomainType.Internal, domains)
.putAll(DomainType.Detected, detectedDomains)
.putAll(DomainType.DetectedIp, ips);
Optional.ofNullable(defaultDomain)
.ifPresent(domain -> result.put(DomainType.DefaultDomain, domain));
return result.build();
}
private ImmutableList<Domain> detectIps(Collection<Domain> domains) {
if (configuration.isAutoDetectIp()) {
return getDomainsIpStream(domains, dns, LOGGER)
.collect(ImmutableList.toImmutableList());
}
return ImmutableList.of();
}
private ImmutableList<Domain> detectDomains() {
if (configuration.isAutoDetect()) {
String hostName;
try {
hostName = removeTrailingDot(dns.getHostName(dns.getLocalHost()));
} catch (UnknownHostException ue) {
hostName = "localhost";
}
LOGGER.info("Local host is: {}", hostName);
if (!Strings.isNullOrEmpty(hostName) && !hostName.equals("localhost")) {
return ImmutableList.of(Domain.of(hostName));
}
}
return ImmutableList.of();
}
private String removeTrailingDot(String domain) {
if (domain != null && domain.endsWith(".")) {
return domain.substring(0, domain.length() - 1);
}
return domain;
}
/**
* Return a stream of all IP addresses of the given domains.
*
* @param domains
* Iterable of domains
* @return Stream of ipaddress for domains
*/
private static Stream<Domain> getDomainsIpStream(Collection<Domain> domains, DNSService dns, Logger log) {
return domains.stream()
.flatMap(domain -> getDomainIpStream(domain, dns, log))
.distinct();
}
private static Stream<Domain> getDomainIpStream(Domain domain, DNSService dns, Logger log) {
try {
return dns.getAllByName(domain.name()).stream()
.map(InetAddress::getHostAddress)
.map(Domain::of)
.distinct();
} catch (UnknownHostException e) {
log.error("Cannot get IP address(es) for {}", domain);
return Stream.of();
}
}
@Override
public void removeDomain(Domain domain) throws DomainListException {
if (isAutoDetected(domain)) {
throw new AutoDetectedDomainRemovalException(domain);
}
doRemoveDomain(domain);
}
private boolean isAutoDetected(Domain domain) throws DomainListException {
Multimap<DomainType, Domain> domainsWithType = getDomainsWithType();
return domainsWithType.get(DomainType.Detected).contains(domain)
|| domainsWithType.get(DomainType.DetectedIp).contains(domain)
|| domainsWithType.get(DomainType.DefaultDomain).contains(domain);
}
/**
* Return domainList
*
* @return List
*/
protected abstract List<Domain> getDomainListInternal() throws DomainListException;
protected abstract boolean containsDomainInternal(Domain domain) throws DomainListException;
protected Mono<Boolean> containsDomainInternalReactive(Domain domain) {
return Mono.fromCallable(() -> containsDomainInternal(domain))
.subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER);
}
protected abstract void doRemoveDomain(Domain domain) throws DomainListException;
}