blob: 3a246a827821b5fa1eb4474f1308421311abd7e3 [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.jmap.draft.methods;
import static org.apache.james.jmap.methods.Method.JMAP_PREFIX;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import jakarta.inject.Inject;
import org.apache.james.jmap.draft.exceptions.MailboxNotOwnedException;
import org.apache.james.jmap.draft.exceptions.MailboxParentNotFoundException;
import org.apache.james.jmap.draft.model.MailboxCreationId;
import org.apache.james.jmap.draft.model.MailboxFactory;
import org.apache.james.jmap.draft.model.SetError;
import org.apache.james.jmap.draft.model.SetMailboxesRequest;
import org.apache.james.jmap.draft.model.SetMailboxesResponse;
import org.apache.james.jmap.draft.model.mailbox.Mailbox;
import org.apache.james.jmap.draft.model.mailbox.MailboxCreateRequest;
import org.apache.james.jmap.draft.utils.DependencyGraph.CycleDetectedException;
import org.apache.james.jmap.draft.utils.SortingHierarchicalCollections;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.exception.InboxAlreadyCreated;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxExistsException;
import org.apache.james.mailbox.exception.MailboxNameException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.exception.TooLongMailboxNameException;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxId.Factory;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.metrics.api.TimeMetric;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.fge.lambdas.Throwing;
import com.github.fge.lambdas.functions.FunctionChainer;
import com.google.common.annotations.VisibleForTesting;
public class SetMailboxesCreationProcessor implements SetMailboxesProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(SetMailboxesCreationProcessor.class);
private final MailboxManager mailboxManager;
private final SortingHierarchicalCollections<Map.Entry<MailboxCreationId, MailboxCreateRequest>, MailboxCreationId> sortingHierarchicalCollections;
private final MailboxFactory mailboxFactory;
private final Factory mailboxIdFactory;
private final MetricFactory metricFactory;
@Inject
@VisibleForTesting
SetMailboxesCreationProcessor(MailboxManager mailboxManager, MailboxFactory mailboxFactory, Factory mailboxIdFactory, MetricFactory metricFactory) {
this.mailboxManager = mailboxManager;
this.metricFactory = metricFactory;
this.sortingHierarchicalCollections =
new SortingHierarchicalCollections<>(
Map.Entry::getKey,
x -> x.getValue().getParentId());
this.mailboxFactory = mailboxFactory;
this.mailboxIdFactory = mailboxIdFactory;
}
@Override
public SetMailboxesResponse process(SetMailboxesRequest request, MailboxSession mailboxSession) {
TimeMetric timeMetric = metricFactory.timer(JMAP_PREFIX + "SetMailboxesCreationProcessor");
SetMailboxesResponse.Builder builder = SetMailboxesResponse.builder();
try {
Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId = new HashMap<>();
sortingHierarchicalCollections.sortFromRootToLeaf(request.getCreate().entrySet())
.forEach(entry ->
createMailbox(entry.getKey(), entry.getValue(), mailboxSession, creationIdsToCreatedMailboxId, builder));
} catch (CycleDetectedException e) {
markRequestsAsNotCreatedDueToCycle(request, builder);
}
timeMetric.stopAndPublish();
return builder.build();
}
private void markRequestsAsNotCreatedDueToCycle(SetMailboxesRequest request, SetMailboxesResponse.Builder builder) {
request.getCreate().forEach((key, value) ->
builder.notCreated(
key,
SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description("The created mailboxes introduce a cycle.")
.build()));
}
private void createMailbox(MailboxCreationId mailboxCreationId, MailboxCreateRequest mailboxRequest, MailboxSession mailboxSession,
Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId, SetMailboxesResponse.Builder builder) {
try {
ensureValidMailboxName(mailboxRequest, mailboxSession);
MailboxPath mailboxPath = computeMailboxPath(mailboxRequest, creationIdsToCreatedMailboxId, mailboxSession);
Optional<MailboxId> mailboxId = mailboxManager.createMailbox(mailboxPath, MailboxManager.CreateOption.CREATE_SUBSCRIPTION, mailboxSession);
Optional<Mailbox> mailbox = mailboxId.flatMap(id -> mailboxFactory.builder()
.id(id)
.session(mailboxSession)
.build()
.blockOptional());
if (mailbox.isPresent()) {
builder.created(mailboxCreationId, mailbox.get());
creationIdsToCreatedMailboxId.put(mailboxCreationId, mailbox.get().getId());
} else {
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.ERROR)
.description("An error occurred when creating the mailbox")
.build());
}
} catch (TooLongMailboxNameException e) {
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description("The mailbox name length is too long")
.build());
} catch (MailboxNotOwnedException e) {
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description("The mailbox can not be created with a parent mailbox belonging to another user")
.build());
} catch (MailboxNameException | MailboxParentNotFoundException e) {
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description(e.getMessage())
.build());
} catch (InboxAlreadyCreated e) {
String message = String.format("The mailbox '%s' already exists as 'INBOX'", e.getMailboxName());
LOGGER.error(message, e);
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description(message)
.build());
} catch (MailboxExistsException e) {
String message = String.format("The mailbox '%s' already exists.", mailboxCreationId.getCreationId());
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description(message)
.build());
} catch (MailboxException e) {
String message = String.format("An error occurred when creating the mailbox '%s'", mailboxCreationId.getCreationId());
LOGGER.error(message, e);
builder.notCreated(mailboxCreationId, SetError.builder()
.type(SetError.Type.ERROR)
.description(message)
.build());
}
}
private void ensureValidMailboxName(MailboxCreateRequest mailboxRequest, MailboxSession mailboxSession) throws MailboxNameException {
String name = mailboxRequest.getName();
char pathDelimiter = mailboxSession.getPathDelimiter();
if (name.contains(String.valueOf(pathDelimiter))) {
throw new MailboxNameException(String.format("The mailbox '%s' contains an illegal character: '%c'", name, pathDelimiter));
}
}
private MailboxPath computeMailboxPath(MailboxCreateRequest mailboxRequest, Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId, MailboxSession mailboxSession) throws MailboxException {
if (mailboxRequest.getParentId().isPresent()) {
MailboxCreationId parentId = mailboxRequest.getParentId().get();
MailboxPath parentPath = getMailboxPath(creationIdsToCreatedMailboxId, mailboxSession, parentId);
assertBelongsToUser(parentPath, mailboxSession);
return MailboxPath.forUser(mailboxSession.getUser(),
parentPath.getName() + mailboxSession.getPathDelimiter() + mailboxRequest.getName());
}
return MailboxPath.forUser(mailboxSession.getUser(), mailboxRequest.getName());
}
private void assertBelongsToUser(MailboxPath mailboxPath, MailboxSession mailboxSession) throws MailboxNotOwnedException {
if (!mailboxPath.belongsTo(mailboxSession)) {
throw new MailboxNotOwnedException();
}
}
private MailboxPath getMailboxPath(Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId, MailboxSession mailboxSession, MailboxCreationId parentId) throws MailboxException {
Optional<MailboxId> mailboxId = readCreationIdAsMailboxId(parentId)
.or(() -> Optional.ofNullable(creationIdsToCreatedMailboxId.get(parentId)));
return getMailboxPathFromId(mailboxId, mailboxSession)
.orElseThrow(() -> new MailboxParentNotFoundException(parentId));
}
private Optional<MailboxId> readCreationIdAsMailboxId(MailboxCreationId creationId) {
try {
return Optional.of(mailboxIdFactory.fromString(creationId.getCreationId()));
} catch (Exception e) {
return Optional.empty();
}
}
@VisibleForTesting
Optional<MailboxPath> getMailboxPathFromId(Optional<MailboxId> mailboxId, MailboxSession mailboxSession) {
FunctionChainer<MailboxId, Optional<MailboxPath>> fromMailboxIdToMailboxPath = Throwing.function(id -> {
try {
return Optional.of(mailboxManager.getMailbox(id, mailboxSession).getMailboxPath());
} catch (MailboxNotFoundException e) {
return Optional.empty();
}
});
return mailboxId
.flatMap(fromMailboxIdToMailboxPath.sneakyThrow());
}
}