blob: a98a27e1eab6f9c6f7d0612773841dc0cecd7e5f [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.imap.processor;
import static org.apache.james.imap.message.request.ListRequest.ListSelectOption.RECURSIVEMATCH;
import static org.apache.james.imap.message.request.ListRequest.ListSelectOption.SPECIAL_USE;
import static org.apache.james.mailbox.MailboxManager.MailboxSearchFetchType.Minimal;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.james.imap.api.display.HumanReadableText;
import org.apache.james.imap.api.display.ModifiedUtf7;
import org.apache.james.imap.api.message.Capability;
import org.apache.james.imap.api.message.StatusDataItems;
import org.apache.james.imap.api.message.response.ImapResponseMessage;
import org.apache.james.imap.api.message.response.StatusResponseFactory;
import org.apache.james.imap.api.process.ImapSession;
import org.apache.james.imap.api.process.MailboxType;
import org.apache.james.imap.api.process.MailboxTyper;
import org.apache.james.imap.main.PathConverter;
import org.apache.james.imap.message.request.ListRequest;
import org.apache.james.imap.message.response.ListResponse;
import org.apache.james.imap.message.response.MailboxStatusResponse;
import org.apache.james.imap.message.response.MyRightsResponse;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.SubscriptionManager;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.MailboxACL;
import org.apache.james.mailbox.model.MailboxConstants;
import org.apache.james.mailbox.model.MailboxMetaData;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.search.MailboxQuery;
import org.apache.james.mailbox.model.search.PrefixedRegex;
import org.apache.james.mailbox.model.search.Wildcard;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.util.MDCBuilder;
import org.apache.james.util.ReactorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.fge.lambdas.Throwing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class ListProcessor<T extends ListRequest> extends AbstractMailboxProcessor<T> implements CapabilityImplementingProcessor {
public static final boolean RETURN_SUBSCRIBED = true;
public static final boolean RETURN_NON_EXISTENT = true;
private static final Logger LOGGER = LoggerFactory.getLogger(ListProcessor.class);
private static final List<Capability> CAPA = ImmutableList.of(
Capability.of("LIST-EXTENDED"),
Capability.of("LIST-STATUS"),
Capability.of("LIST-MYRIGHTS"),
Capability.of("SPECIAL-USE"));
private final SubscriptionManager subscriptionManager;
private final StatusProcessor statusProcessor;
protected final MailboxTyper mailboxTyper;
@Inject
public ListProcessor(MailboxManager mailboxManager, StatusResponseFactory factory,
MetricFactory metricFactory, SubscriptionManager subscriptionManager,
StatusProcessor statusProcessor, MailboxTyper mailboxTyper) {
this((Class<T>) ListRequest.class, mailboxManager, factory, metricFactory, subscriptionManager, statusProcessor, mailboxTyper);
}
public ListProcessor(Class<T> clazz, MailboxManager mailboxManager, StatusResponseFactory factory,
MetricFactory metricFactory, SubscriptionManager subscriptionManager,
StatusProcessor statusProcessor, MailboxTyper mailboxTyper) {
super(clazz, mailboxManager, factory, metricFactory);
this.subscriptionManager = subscriptionManager;
this.statusProcessor = statusProcessor;
this.mailboxTyper = mailboxTyper;
}
@Override
public List<Capability> getImplementedCapabilities(ImapSession session) {
return CAPA;
}
/**
* (from rfc3501)<br>
* The LIST command returns a subset of names from the complete set of all
* names available to the client. Zero or more untagged LIST replies are
* returned, containing the name attributes, hierarchy delimiter, and name;
* see the description of the LIST reply for more detail.<br>
* ...<br>
* An empty ("" string) mailbox name argument is a special request to return
* the hierarchy delimiter and the root name of the name given in the
* reference. The value returned as the root MAY be the empty string if the
* reference is non-rooted or is an empty string.
*/
@Override
protected Mono<Void> processRequestReactive(T request, ImapSession session, Responder responder) {
MailboxSession mailboxSession = session.getMailboxSession();
return respond(session, responder, request, mailboxSession)
.then(Mono.fromRunnable(() -> okComplete(request, responder)))
.onErrorResume(MailboxException.class, e -> {
no(request, responder, HumanReadableText.SEARCH_FAILED);
return ReactorUtils.logAsMono(() -> LOGGER.error("List failed for mailboxName {}", request.getMailboxPattern(), e));
})
.then();
}
private Mono<Void> respond(ImapSession session, Responder responder, T request, MailboxSession mailboxSession) {
if (request.getMailboxPattern().length() == 0) {
return Mono.fromRunnable(() -> respondNamespace(request.getBaseReferenceName(), responder, mailboxSession));
} else {
return respondMailboxList(request, session, responder, mailboxSession);
}
}
protected ImapResponseMessage createResponse(MailboxMetaData.Children children, MailboxMetaData.Selectability selectability, String name,
char hierarchyDelimiter, MailboxType type, boolean isSubscribed) {
return new ListResponse(children, selectability, name, hierarchyDelimiter, isSubscribed,
!RETURN_NON_EXISTENT, EnumSet.noneOf(ListResponse.ChildInfo.class), type);
}
private void respondNamespace(String referenceName, Responder responder, MailboxSession mailboxSession) {
// An empty mailboxName signifies a request for the hierarchy
// delimiter and root name of the referenceName argument
String referenceRoot = ModifiedUtf7.decodeModifiedUTF7(computeReferenceRoot(referenceName, mailboxSession));
responder.respond(createResponse(
MailboxMetaData.Children.CHILDREN_ALLOWED_BUT_UNKNOWN,
MailboxMetaData.Selectability.NOSELECT,
referenceRoot,
mailboxSession.getPathDelimiter(),
MailboxType.OTHER,
!RETURN_SUBSCRIBED));
}
private String computeReferenceRoot(String referenceName, MailboxSession mailboxSession) {
if (referenceName.length() > 0 && referenceName.charAt(0) == MailboxConstants.NAMESPACE_PREFIX_CHAR) {
// A qualified reference name - get the root element
int firstDelimiter = referenceName.indexOf(mailboxSession.getPathDelimiter());
if (firstDelimiter == -1) {
return referenceName;
} else {
return referenceName.substring(0, firstDelimiter);
}
} else {
// A relative reference name, return "" to indicate it is
// non-rooted
return "";
}
}
private Mono<Void> respondMailboxList(T request, ImapSession session,
Responder responder, MailboxSession mailboxSession) {
if (request.selectRemote()) {
// https://www.rfc-editor.org/rfc/rfc5258.html. NOT YET SUPPORT `REMOTE`
return Mono.empty();
}
// If the mailboxPattern is fully qualified, ignore the
// reference name.
String finalReferencename = request.getBaseReferenceName();
if (request.getMailboxPattern().charAt(0) == MailboxConstants.NAMESPACE_PREFIX_CHAR) {
finalReferencename = "";
}
// Is the interpreted (combined) pattern relative?
// Should the namespace section be returned or not?
boolean isRelative = ((finalReferencename + request.getMailboxPattern()).charAt(0) != MailboxConstants.NAMESPACE_PREFIX_CHAR);
MailboxQuery mailboxQuery = mailboxQuery(computeBasePath(session, finalReferencename, isRelative),
request.getMailboxPattern(), mailboxSession);
if (request.selectSubscribed()) {
return processWithSubscribed(session, request, responder, mailboxSession, isRelative, mailboxQuery);
} else if (request.getReturnOptions().contains(ListRequest.ListReturnOption.SUBSCRIBED)) {
return Flux.from(Throwing.supplier(() -> subscriptionManager.subscriptionsReactive(mailboxSession)).get())
.collect(ImmutableMap.toImmutableMap(path -> path, path -> path))
.flatMap(subscribed -> processWithoutSubscribed(session, request, responder, mailboxSession, isRelative, mailboxQuery, subscribed::containsKey));
} else {
return processWithoutSubscribed(session, request, responder, mailboxSession, isRelative, mailboxQuery, any -> false);
}
}
private Mono<Void> processWithoutSubscribed(ImapSession session, T request, Responder responder, MailboxSession mailboxSession,
boolean isRelative, MailboxQuery mailboxQuery, Predicate<MailboxPath> isSubscribed) {
return getMailboxManager().search(mailboxQuery, Minimal, mailboxSession)
.doOnNext(metaData -> {
MailboxType mailboxType = getMailboxType(request, session, metaData.getPath());
if (!request.getSelectOptions().contains(SPECIAL_USE) || mailboxType.getRfc6154attributeName() != null) {
responder.respond(
createResponse(metaData.inferiors(),
metaData.getSelectability(),
mailboxName(isRelative, metaData.getPath(), metaData.getHierarchyDelimiter()),
metaData.getHierarchyDelimiter(),
mailboxType,
isSubscribed.test(metaData.getPath())));
}
})
.doOnNext(metaData -> respondMyRights(request, responder, mailboxSession, metaData))
.concatMap(metaData -> request.getStatusDataItems().map(statusDataItems -> statusProcessor.sendStatus(retrieveMessageManager(metaData, mailboxSession), statusDataItems, responder, session, mailboxSession)).orElse(Mono.empty()))
.then();
}
private MessageManager retrieveMessageManager(MailboxMetaData metaData, MailboxSession mailboxSession) {
try {
return getMailboxManager().getMailbox(metaData.getMailbox(), mailboxSession);
} catch (MailboxException e) {
throw new RuntimeException(e);
}
}
private Mono<Void> processWithSubscribed(ImapSession session, T request, Responder responder, MailboxSession mailboxSession, boolean isRelative, MailboxQuery mailboxQuery) {
return Mono.zip(getMailboxManager().search(mailboxQuery, Minimal, mailboxSession).collectList()
.map(searchedResultList -> searchedResultList.stream().collect(Collectors.toMap(MailboxMetaData::getPath, Function.identity()))),
Flux.from(Throwing.supplier(() -> subscriptionManager.subscriptionsReactive(mailboxSession)).get()).collectList())
.map(tuple -> getListResponseForSelectSubscribed(session, tuple.getT1(), tuple.getT2(), request, mailboxSession, isRelative, mailboxQuery))
.flatMapIterable(list -> list)
.doOnNext(pathAndResponse -> responder.respond(pathAndResponse.getMiddle()))
.doOnNext(pathAndResponse -> pathAndResponse.getRight().ifPresent(mailboxMetaData -> respondMyRights(request, responder, mailboxSession, mailboxMetaData)))
.concatMap(pathAndResponse -> sendStatusWhenSubscribed(session, request, responder, mailboxSession, pathAndResponse))
.then();
}
private Mono<MailboxStatusResponse> sendStatusWhenSubscribed(ImapSession session, T request, Responder responder, MailboxSession mailboxSession,
Triple<MailboxPath, ListResponse, Optional<MailboxMetaData>> pathAndResponse) {
return pathAndResponse.getRight()
.map(metaData -> retrieveMessageManager(metaData, mailboxSession))
.flatMap(messageManager -> request.getStatusDataItems()
.map(statusDataItems -> statusProcessor.sendStatus(messageManager, statusDataItems, responder, session, mailboxSession)))
.orElse(Mono.empty());
}
private List<Triple<MailboxPath, ListResponse, Optional<MailboxMetaData>>> getListResponseForSelectSubscribed(ImapSession session, Map<MailboxPath, MailboxMetaData> searchedResultMap, List<MailboxPath> allSubscribedSearch,
ListRequest listRequest, MailboxSession mailboxSession, boolean relative, MailboxQuery mailboxQuery) {
ImmutableList.Builder<Triple<MailboxPath, ListResponse, Optional<MailboxMetaData>>> responseBuilders = ImmutableList.builder();
List<Pair<MailboxPath, ListResponse>> listRecursiveMatch = listRecursiveMatch(session, searchedResultMap, allSubscribedSearch, mailboxSession, relative, listRequest);
listRecursiveMatch.forEach(pair -> responseBuilders.add(Triple.of(pair.getLeft(), pair.getRight(), Optional.ofNullable(searchedResultMap.get(pair.getLeft())))));
Set<MailboxPath> listRecursiveMatchPath = listRecursiveMatch.stream().map(Pair::getKey).collect(Collectors.toUnmodifiableSet());
allSubscribedSearch.stream()
.filter(subscribed -> !listRecursiveMatchPath.contains(subscribed))
.filter(mailboxQuery::isPathMatch)
.map(subscribed -> buildListResponse(listRequest, searchedResultMap, session, relative, subscribed))
.filter(pair -> !listRequest.getSelectOptions().contains(SPECIAL_USE) || mailboxTyper.getMailboxType(session, pair.getKey()).getRfc6154attributeName() != null)
.forEach(pair -> responseBuilders.add(Triple.of(pair.getLeft(), pair.getRight(), Optional.ofNullable(searchedResultMap.get(pair.getLeft())))));
return responseBuilders.build();
}
private Pair<MailboxPath, ListResponse> buildListResponse(ListRequest listRequest, Map<MailboxPath, MailboxMetaData> searchedResultMap, ImapSession session, boolean relative, MailboxPath subscribed) {
return Pair.of(subscribed, Optional.ofNullable(searchedResultMap.get(subscribed))
.map(mailboxMetaData -> ListResponse.builder()
.returnSubscribed(RETURN_SUBSCRIBED)
.forMetaData(mailboxMetaData)
.name(mailboxName(relative, subscribed, mailboxMetaData.getHierarchyDelimiter()))
.returnNonExistent(!RETURN_NON_EXISTENT)
.mailboxType(getMailboxType(listRequest, session, mailboxMetaData.getPath())))
.orElseGet(() -> ListResponse.builder().nonExitingSubscribedMailbox(subscribed))
.build());
}
private List<Pair<MailboxPath, ListResponse>> listRecursiveMatch(ImapSession session, Map<MailboxPath, MailboxMetaData> searchedResultMap,
List<MailboxPath> allSubscribedSearch, MailboxSession mailboxSession,
boolean relative, ListRequest listRequest) {
if (!listRequest.getSelectOptions().contains(RECURSIVEMATCH)) {
return List.of();
}
Set<MailboxPath> allSubscribedSearchParent = allSubscribedSearch.stream()
.flatMap(mailboxPath -> mailboxPath.getParents(mailboxSession.getPathDelimiter()).stream())
.collect(Collectors.toSet());
return searchedResultMap.entrySet().stream()
.filter(pair -> allSubscribedSearchParent.contains(pair.getKey()))
.map(pair -> {
MailboxMetaData metaData = pair.getValue();
ListResponse listResponse = ListResponse.builder()
.forMetaData(metaData)
.name(mailboxName(relative, metaData.getPath(), metaData.getHierarchyDelimiter()))
.childInfos(ListResponse.ChildInfo.SUBSCRIBED)
.returnSubscribed(allSubscribedSearch.contains(pair.getKey()))
.mailboxType(getMailboxType(listRequest, session, metaData.getPath()))
.build();
return Pair.of(pair.getKey(), listResponse);
})
.collect(Collectors.toList());
}
private void respondMyRights(T request, Responder responder, MailboxSession mailboxSession, MailboxMetaData metaData) {
if (request.getReturnOptions().contains(ListRequest.ListReturnOption.MYRIGHTS)) {
MyRightsResponse myRightsResponse = new MyRightsResponse(metaData.getPath().getName(), getRfc4314Rights(mailboxSession, metaData));
responder.respond(myRightsResponse);
}
}
private MailboxACL.Rfc4314Rights getRfc4314Rights(MailboxSession mailboxSession, MailboxMetaData metaData) {
if (metaData.getPath().belongsTo(mailboxSession)) {
return MailboxACL.FULL_RIGHTS;
}
MailboxACL.EntryKey entryKey = MailboxACL.EntryKey.createUserEntryKey(mailboxSession.getUser());
return metaData.getResolvedAcls().getEntries().get(entryKey);
}
private MailboxQuery mailboxQuery(MailboxPath basePath, String mailboxName, MailboxSession mailboxSession) {
if (basePath.getNamespace().equals(MailboxConstants.USER_NAMESPACE)
&& basePath.getUser().equals(mailboxSession.getUser())
&& basePath.getName().isEmpty()
&& mailboxName.equals("*")) {
return MailboxQuery.builder()
.userAndNamespaceFrom(basePath)
.expression(Wildcard.INSTANCE)
.build();
}
return MailboxQuery.builder()
.userAndNamespaceFrom(basePath)
.expression(new PrefixedRegex(
basePath.getName(),
ModifiedUtf7.decodeModifiedUTF7(mailboxName),
mailboxSession.getPathDelimiter()))
.build();
}
private MailboxPath computeBasePath(ImapSession session, String finalReferencename, boolean isRelative) {
String decodedName = ModifiedUtf7.decodeModifiedUTF7(finalReferencename);
if (isRelative) {
return MailboxPath.forUser(session.getUserName(), decodedName);
} else {
return PathConverter.forSession(session).buildFullPath(decodedName);
}
}
/**
* retrieve mailboxType for specified mailboxPath using provided
* MailboxTyper
*
* @param session current imap session
* @param path mailbox's path
* @return MailboxType value
*/
protected MailboxType getMailboxType(ListRequest listRequest, ImapSession session, MailboxPath path) {
if (listRequest.getReturnOptions().contains(ListRequest.ListReturnOption.SPECIAL_USE)) {
return mailboxTyper.getMailboxType(session, path);
}
return MailboxType.OTHER;
}
@Override
protected MDCBuilder mdc(T request) {
return MDCBuilder.create()
.addToContext(MDCBuilder.ACTION, "LIST")
.addToContext("base", request.getBaseReferenceName())
.addToContext("pattern", request.getMailboxPattern())
.addToContext("returnOptions", request.getReturnOptions().toString())
.addToContext("selectOptions", request.getSelectOptions().toString())
.addToContextIfPresent("statusItems", request.getStatusDataItems().map(StatusDataItems::toString));
}
}