blob: 175e3b3a457d9d78ab628efd4bd75577a4255f3b [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.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.inject.Inject;
import jakarta.mail.MessagingException;
import org.apache.james.core.Username;
import org.apache.james.jmap.draft.exceptions.AttachmentsNotFoundException;
import org.apache.james.jmap.draft.exceptions.InvalidDraftKeywordsException;
import org.apache.james.jmap.draft.exceptions.InvalidMailboxForCreationException;
import org.apache.james.jmap.draft.exceptions.MailboxNotOwnedException;
import org.apache.james.jmap.draft.exceptions.SizeExceededException;
import org.apache.james.jmap.draft.methods.ValueWithId.CreationMessageEntry;
import org.apache.james.jmap.draft.methods.ValueWithId.MessageWithId;
import org.apache.james.jmap.draft.model.CreationMessage;
import org.apache.james.jmap.draft.model.CreationMessage.DraftEmailer;
import org.apache.james.jmap.draft.model.EnvelopeUtils;
import org.apache.james.jmap.draft.model.SetError;
import org.apache.james.jmap.draft.model.SetMessagesError;
import org.apache.james.jmap.draft.model.SetMessagesRequest;
import org.apache.james.jmap.draft.model.SetMessagesResponse;
import org.apache.james.jmap.draft.model.SetMessagesResponse.Builder;
import org.apache.james.jmap.model.MessageProperties;
import org.apache.james.jmap.model.MessageProperties.MessageProperty;
import org.apache.james.jmap.model.message.view.MessageFullView;
import org.apache.james.jmap.model.message.view.MessageFullViewFactory;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.Role;
import org.apache.james.mailbox.SystemMailboxesProvider;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.exception.OverQuotaException;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.rrt.api.CanSendFrom;
import org.apache.james.server.core.Envelope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.fge.lambdas.Throwing;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class SetMessagesCreationProcessor implements SetMessagesProcessor {
private static final Logger LOG = LoggerFactory.getLogger(SetMailboxesCreationProcessor.class);
private final MessageFullViewFactory messageFullViewFactory;
private final SystemMailboxesProvider systemMailboxesProvider;
private final MetricFactory metricFactory;
private final MailboxManager mailboxManager;
private final MailboxId.Factory mailboxIdFactory;
private final MessageAppender messageAppender;
private final MessageSender messageSender;
private final ReferenceUpdater referenceUpdater;
private final CanSendFrom canSendFrom;
@VisibleForTesting
@Inject
SetMessagesCreationProcessor(MessageFullViewFactory messageFullViewFactory,
SystemMailboxesProvider systemMailboxesProvider,
MetricFactory metricFactory,
MailboxManager mailboxManager,
MailboxId.Factory mailboxIdFactory,
MessageAppender messageAppender,
MessageSender messageSender,
ReferenceUpdater referenceUpdater,
CanSendFrom canSendFrom) {
this.messageFullViewFactory = messageFullViewFactory;
this.systemMailboxesProvider = systemMailboxesProvider;
this.metricFactory = metricFactory;
this.mailboxManager = mailboxManager;
this.mailboxIdFactory = mailboxIdFactory;
this.messageAppender = messageAppender;
this.messageSender = messageSender;
this.referenceUpdater = referenceUpdater;
this.canSendFrom = canSendFrom;
}
@Override
public Mono<SetMessagesResponse> processReactive(SetMessagesRequest request, MailboxSession mailboxSession) {
if (request.getCreate().isEmpty()) {
return Mono.just(SetMessagesResponse.builder().build());
}
return Mono.from(metricFactory.decoratePublisherWithTimerMetric(JMAP_PREFIX + "SetMessageCreationProcessor",
Flux.fromIterable(request.getCreate())
.flatMap(create -> handleCreate(create, mailboxSession))
.reduce(Builder::mergeWith)
.switchIfEmpty(Mono.just(SetMessagesResponse.builder()))
.map(Builder::build)));
}
private Mono<Builder> handleCreate(CreationMessageEntry create, MailboxSession mailboxSession) {
List<MailboxId> mailboxIds = toMailboxIds(create);
if (mailboxIds.isEmpty()) {
return Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.mailboxIds)
.description("Message needs to be in at least one mailbox")
.build()));
}
return assertIsUserOwnerOfMailboxes(mailboxIds, mailboxSession)
.then(performCreate(create, mailboxSession))
.onErrorResume(MailboxSendingNotAllowedException.class, e -> {
LOG.debug("{} is not allowed to send a mail using {} identity", e.getConnectedUser().asString(), e.getFromField());
return Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.from)
.description("Invalid 'from' field. One accepted value is " +
e.getConnectedUser().asString())
.build()));
})
.onErrorResume(InvalidDraftKeywordsException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.keywords)
.description(e.getMessage())
.build())))
.onErrorResume(SizeExceededException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_ARGUMENTS)
.description(e.getMessage())
.build())))
.onErrorResume(AttachmentsNotFoundException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetMessagesError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.attachments)
.attachmentsNotFound(e.getAttachmentIds())
.description("Attachment not found")
.build())))
.onErrorResume(InvalidMailboxForCreationException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(MessageProperty.mailboxIds)
.description("Message creation is only supported in mailboxes with role Draft and Outbox")
.build())))
.onErrorResume(MailboxInvalidMessageCreationException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
buildSetErrorFromValidationResult(create.getValue().validate()))))
.onErrorResume(MailboxNotFoundException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.description(e.getMessage())
.build())))
.onErrorResume(MailboxNotOwnedException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.properties(MessageProperty.mailboxIds)
.description("MailboxId invalid")
.build())))
.onErrorResume(OverQuotaException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.MAX_QUOTA_REACHED)
.description(e.getMessage())
.build())))
.onErrorResume(MailboxException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.description("unexpected error")
.build())))
.onErrorResume(MessagingException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.description("unexpected error")
.build())))
.onErrorResume(IOException.class, e -> Mono.just(SetMessagesResponse.builder().notCreated(create.getCreationId(),
SetError.builder()
.type(SetError.Type.ERROR)
.description("unexpected error")
.build())));
}
private ImmutableList<MailboxId> toMailboxIds(CreationMessageEntry create) {
return create.getValue().getMailboxIds()
.stream()
.distinct()
.map(mailboxIdFactory::fromString)
.collect(ImmutableList.toImmutableList());
}
private Mono<Builder> performCreate(CreationMessageEntry entry, MailboxSession session) {
return isAppendToMailboxWithRole(Role.OUTBOX, entry.getValue(), session)
.flatMap(isAppendToMailboxWithRole -> {
if (isAppendToMailboxWithRole) {
return sendMailViaOutbox(entry, session);
} else if (entry.getValue().isDraft()) {
return assertNoOutbox(entry, session)
.then(saveDraft(entry, session));
} else {
return isAppendToMailboxWithRole(Role.DRAFTS, entry.getValue(), session)
.handle((isAppendedToDraft, sink) -> {
if (isAppendedToDraft) {
sink.error(new InvalidDraftKeywordsException("A draft message should be flagged as Draft"));
} else {
sink.error(new InvalidMailboxForCreationException("The only implemented feature is sending via outbox and draft saving"));
}
});
}
});
}
private Mono<Void> assertNoOutbox(CreationMessageEntry entry, MailboxSession session) {
return isTargettingAMailboxWithRole(Role.OUTBOX, entry.getValue(), session)
.handle((targetsOutbox, sink) -> {
if (targetsOutbox) {
sink.error(new InvalidMailboxForCreationException("Mailbox ids can combine Outbox with other mailbox"));
}
});
}
private Mono<Builder> sendMailViaOutbox(CreationMessageEntry entry, MailboxSession session) {
if (!entry.getValue().isValid()) {
return Mono.error(new MailboxInvalidMessageCreationException());
}
return handleOutboxMessages(entry, session)
.map(created -> SetMessagesResponse.builder().created(created.getCreationId(), created.getValue()));
}
private Mono<Builder> saveDraft(CreationMessageEntry entry, MailboxSession session) {
return handleDraftMessages(entry, session)
.map(created -> SetMessagesResponse.builder().created(created.getCreationId(), created.getValue()));
}
@VisibleForTesting Mono<Void> assertIsUserOwnerOfMailboxes(List<MailboxId> mailboxIds, MailboxSession session) {
return allMailboxOwned(mailboxIds, session)
.handle((allOwned, sink) -> {
if (!allOwned) {
sink.error(new MailboxNotOwnedException());
}
});
}
private Mono<Boolean> allMailboxOwned(List<MailboxId> mailboxIds, MailboxSession session) {
return Flux.fromIterable(mailboxIds)
.concatMap(id -> mailboxManager.getMailboxReactive(id, session))
.map(Throwing.function(MessageManager::getMailboxPath))
.all(path -> path.belongsTo(session));
}
private Mono<MessageWithId> handleOutboxMessages(CreationMessageEntry entry, MailboxSession session) {
return assertUserCanSendFrom(session.getUser(), entry.getValue().getFrom())
.then(messageAppender.appendMessageInMailboxes(entry, toMailboxIds(entry), session))
.flatMap(newMessage ->
messageFullViewFactory.fromMetaDataWithContent(newMessage)
.flatMap(Throwing.function((MessageFullView jmapMessage) -> {
Envelope envelope = EnvelopeUtils.fromMessage(jmapMessage);
return messageSender.sendMessage(newMessage, envelope, session)
.then(referenceUpdater.updateReferences(entry.getValue().getHeaders(), session))
.thenReturn(new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage));
}).sneakyThrow()));
}
@VisibleForTesting
Mono<Void> assertUserCanSendFrom(Username connectedUser, Optional<DraftEmailer> from) {
Optional<Username> maybeFromUser = from.flatMap(DraftEmailer::getEmail)
.map(Username::of);
return Mono.from(canSendMailUsingIdentity(connectedUser, maybeFromUser))
.filter(Boolean::booleanValue)
.doOnNext(bool -> LOG.debug("{} is allowed to send a mail using {} identity", connectedUser.asString(), from))
.switchIfEmpty(Mono.error(() -> new MailboxSendingNotAllowedException(connectedUser, maybeFromUser)))
.then();
}
private Mono<Boolean> canSendMailUsingIdentity(Username connectedUser, Optional<Username> maybeFromUser) {
return Mono.justOrEmpty(maybeFromUser)
.flatMap(fromUser -> Mono.from(canSendFrom.userCanSendFromReactive(connectedUser, fromUser)));
}
private Mono<MessageWithId> handleDraftMessages(CreationMessageEntry entry, MailboxSession session) {
return messageAppender.appendMessageInMailboxes(entry, toMailboxIds(entry), session)
.flatMap(messageFullViewFactory::fromMetaDataWithContent)
.map(jmapMessage -> new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage));
}
private Mono<Boolean> isAppendToMailboxWithRole(Role role, CreationMessage entry, MailboxSession mailboxSession) {
return getMailboxWithRole(mailboxSession, role)
.map(entry::isOnlyIn)
.switchIfEmpty(Mono.just(false));
}
private Mono<Boolean> isTargettingAMailboxWithRole(Role role, CreationMessage entry, MailboxSession mailboxSession) {
return getMailboxWithRole(mailboxSession, role)
.map(entry::isIn)
.switchIfEmpty(Mono.just(false));
}
private Mono<MessageManager> getMailboxWithRole(MailboxSession mailboxSession, Role role) {
return Flux.from(systemMailboxesProvider.getMailboxByRole(role, mailboxSession.getUser()))
.next();
}
private SetError buildSetErrorFromValidationResult(List<ValidationResult> validationErrors) {
return SetError.builder()
.type(SetError.Type.INVALID_PROPERTIES)
.properties(collectMessageProperties(validationErrors))
.description(formatValidationErrorMessge(validationErrors))
.build();
}
private String formatValidationErrorMessge(List<ValidationResult> validationErrors) {
return validationErrors.stream()
.map(err -> err.getProperty() + ": " + err.getErrorMessage())
.collect(Collectors.joining("\\n"));
}
private Set<MessageProperties.MessageProperty> collectMessageProperties(List<ValidationResult> validationErrors) {
Splitter propertiesSplitter = Splitter.on(',').trimResults().omitEmptyStrings();
return validationErrors.stream()
.flatMap(err -> propertiesSplitter.splitToStream(err.getProperty()))
.flatMap(MessageProperty::find)
.collect(Collectors.toSet());
}
}