org.apache.james.jmap.methods.SetMessagesCreationProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.jmap.methods.SetMessagesCreationProcessor.java

Source

/****************************************************************
 * 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.methods;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.mail.Flags;
import javax.mail.MessagingException;
import javax.mail.util.SharedByteArrayInputStream;

import org.apache.james.jmap.exceptions.AttachmentsNotFoundException;
import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry;
import org.apache.james.jmap.methods.ValueWithId.MessageWithId;
import org.apache.james.jmap.model.Attachment;
import org.apache.james.jmap.model.BlobId;
import org.apache.james.jmap.model.CreationMessage;
import org.apache.james.jmap.model.CreationMessageId;
import org.apache.james.jmap.model.Message;
import org.apache.james.jmap.model.MessageFactory;
import org.apache.james.jmap.model.MessageFactory.MetaDataWithContent;
import org.apache.james.jmap.model.MessageId;
import org.apache.james.jmap.model.MessageProperties;
import org.apache.james.jmap.model.MessageProperties.MessageProperty;
import org.apache.james.jmap.model.SetError;
import org.apache.james.jmap.model.SetMessagesError;
import org.apache.james.jmap.model.SetMessagesRequest;
import org.apache.james.jmap.model.SetMessagesResponse;
import org.apache.james.jmap.model.SetMessagesResponse.Builder;
import org.apache.james.jmap.model.mailbox.Role;
import org.apache.james.jmap.send.MailFactory;
import org.apache.james.jmap.send.MailMetadata;
import org.apache.james.jmap.send.MailSpool;
import org.apache.james.jmap.utils.SystemMailboxesProvider;
import org.apache.james.lifecycle.api.LifecycleUtil;
import org.apache.james.mailbox.AttachmentManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.exception.AttachmentNotFoundException;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.model.AttachmentId;
import org.apache.james.mailbox.model.Cid;
import org.apache.james.mailbox.model.MessageAttachment;
import org.apache.mailet.Mail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.fge.lambdas.Throwing;
import com.github.fge.lambdas.functions.ThrowingFunction;
import com.github.fge.lambdas.predicates.ThrowingPredicate;
import com.github.steveash.guavate.Guavate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;

public class SetMessagesCreationProcessor implements SetMessagesProcessor {

    private static final Logger LOG = LoggerFactory.getLogger(SetMailboxesCreationProcessor.class);
    private final MIMEMessageConverter mimeMessageConverter;
    private final MailSpool mailSpool;
    private final MailFactory mailFactory;
    private final MessageFactory messageFactory;
    private final SystemMailboxesProvider systemMailboxesProvider;
    private final AttachmentManager attachmentManager;

    @VisibleForTesting
    @Inject
    SetMessagesCreationProcessor(MIMEMessageConverter mimeMessageConverter, MailSpool mailSpool,
            MailFactory mailFactory, MessageFactory messageFactory, SystemMailboxesProvider systemMailboxesProvider,
            AttachmentManager attachmentManager) {
        this.mimeMessageConverter = mimeMessageConverter;
        this.mailSpool = mailSpool;
        this.mailFactory = mailFactory;
        this.messageFactory = messageFactory;
        this.systemMailboxesProvider = systemMailboxesProvider;
        this.attachmentManager = attachmentManager;
    }

    @Override
    public SetMessagesResponse process(SetMessagesRequest request, MailboxSession mailboxSession) {
        Builder responseBuilder = SetMessagesResponse.builder();
        request.getCreate().stream().forEach(create -> handleCreate(create, responseBuilder, mailboxSession));
        return responseBuilder.build();
    }

    private void handleCreate(CreationMessageEntry create, Builder responseBuilder, MailboxSession mailboxSession) {
        try {
            validateImplementedFeature(create, mailboxSession);
            validateArguments(create, mailboxSession);
            validateRights(create, mailboxSession);
            MessageWithId created = handleOutboxMessages(create, mailboxSession);
            responseBuilder.created(created.getCreationId(), created.getValue());

        } catch (MailboxSendingNotAllowedException e) {
            responseBuilder.notCreated(create.getCreationId(),
                    SetError.builder().type("invalidProperties").properties(MessageProperty.from).description(
                            "Invalid 'from' field. Must be one of " + Joiner.on(", ").join(e.getAllowedFroms()))
                            .build());

        } catch (AttachmentsNotFoundException e) {
            responseBuilder.notCreated(create.getCreationId(),
                    SetMessagesError.builder().type("invalidProperties").properties(MessageProperty.mailboxIds)
                            .attachmentsNotFound(e.getAttachmentIds()).description("Attachment not found").build());

        } catch (MailboxNotImplementedException e) {
            responseBuilder.notCreated(create.getCreationId(), SetError.builder().type("invalidProperties")
                    .properties(MessageProperty.mailboxIds).description("Not yet implemented").build());

        } catch (MailboxInvalidMessageCreationException e) {
            responseBuilder.notCreated(create.getCreationId(),
                    buildSetErrorFromValidationResult(create.getValue().validate()));

        } catch (MailboxNotFoundException e) {
            responseBuilder.notCreated(create.getCreationId(),
                    SetError.builder().type("error").description(e.getMailboxName() + " can't be found").build());

        } catch (MailboxException | MessagingException e) {
            LOG.error("Unexpected error while creating message", e);
            responseBuilder.notCreated(create.getCreationId(),
                    SetError.builder().type("error").description("unexpected error").build());
        }
    }

    private void validateImplementedFeature(CreationMessageEntry entry, MailboxSession session)
            throws MailboxException, MailboxNotImplementedException {
        if (isAppendToMailboxWithRole(Role.DRAFTS, entry.getValue(), session)) {
            throw new MailboxNotImplementedException("Drafts saving is not implemented");
        }
        if (!isAppendToMailboxWithRole(Role.OUTBOX, entry.getValue(), session)) {
            throw new MailboxNotImplementedException("The only implemented feature is sending via outbox");
        }
    }

    private void validateArguments(CreationMessageEntry entry, MailboxSession session)
            throws MailboxInvalidMessageCreationException, AttachmentsNotFoundException, MailboxException {
        CreationMessage message = entry.getValue();
        if (!message.isValid()) {
            throw new MailboxInvalidMessageCreationException();
        }
        assertAttachmentsExist(entry, session);
    }

    @VisibleForTesting
    void assertAttachmentsExist(CreationMessageEntry entry, MailboxSession session)
            throws AttachmentsNotFoundException, MailboxException {
        List<Attachment> attachments = entry.getValue().getAttachments();
        if (!attachments.isEmpty()) {
            List<BlobId> notFounds = listAttachmentsNotFound(attachments, session);
            if (!notFounds.isEmpty()) {
                throw new AttachmentsNotFoundException(notFounds);
            }
        }
    }

    private List<BlobId> listAttachmentsNotFound(List<Attachment> attachments, MailboxSession session)
            throws MailboxException {
        ThrowingPredicate<Attachment> notExists = attachment -> {
            try {
                attachmentManager.getAttachment(getAttachmentId(attachment), session);
                return false;
            } catch (AttachmentNotFoundException e) {
                return true;
            }
        };
        return attachments.stream().filter(Throwing.predicate(notExists).sneakyThrow()).map(Attachment::getBlobId)
                .collect(Guavate.toImmutableList());
    }

    private AttachmentId getAttachmentId(Attachment attachment) {
        return AttachmentId.from(attachment.getBlobId().getRawValue());
    }

    private void validateRights(CreationMessageEntry entry, MailboxSession session)
            throws MailboxSendingNotAllowedException {
        List<String> allowedSenders = ImmutableList.of(session.getUser().getUserName());
        if (!isAllowedFromAddress(entry.getValue(), allowedSenders)) {
            throw new MailboxSendingNotAllowedException(allowedSenders);
        }
    }

    private boolean isAllowedFromAddress(CreationMessage creationMessage, List<String> allowedFromMailAddresses) {
        return creationMessage.getFrom()
                .map(draftEmailer -> draftEmailer.getEmail().map(allowedFromMailAddresses::contains).orElse(false))
                .orElse(false);
    }

    private MessageWithId handleOutboxMessages(CreationMessageEntry entry, MailboxSession session)
            throws MailboxException, MessagingException {
        MessageManager outbox = getMailboxWithRole(session, Role.OUTBOX)
                .orElseThrow(() -> new MailboxNotFoundException(Role.OUTBOX.serialize()));
        if (!isRequestForSending(entry.getValue(), session)) {
            throw new IllegalStateException("Messages for everything but outbox should have been filtered earlier");
        }
        MetaDataWithContent newMessage = createMessageInOutbox(entry, outbox, session);
        return sendMessage(entry.getCreationId(), newMessage, session);
    }

    private boolean isAppendToMailboxWithRole(Role role, CreationMessage entry, MailboxSession mailboxSession)
            throws MailboxException {
        return getMailboxWithRole(mailboxSession, role).map(box -> entry.isIn(box)).orElse(false);
    }

    private Optional<MessageManager> getMailboxWithRole(MailboxSession mailboxSession, Role role)
            throws MailboxException {
        return systemMailboxesProvider.listMailboxes(role, mailboxSession).findFirst();
    }

    private SetError buildSetErrorFromValidationResult(List<ValidationResult> validationErrors) {
        return SetError.builder().type("invalidProperties").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.splitToList(err.getProperty()).stream())
                .flatMap(MessageProperty::find).collect(Collectors.toSet());
    }

    private boolean isRequestForSending(CreationMessage creationMessage, MailboxSession session)
            throws MailboxException {
        return isAppendToMailboxWithRole(Role.OUTBOX, creationMessage, session);
    }

    private MetaDataWithContent createMessageInOutbox(MessageWithId.CreationMessageEntry createdEntry,
            MessageManager outbox, MailboxSession session) throws MailboxException {
        ImmutableList<MessageAttachment> messageAttachments = getMessageAttachments(session,
                createdEntry.getValue().getAttachments());
        byte[] messageContent = mimeMessageConverter.convert(createdEntry, messageAttachments);
        SharedByteArrayInputStream content = new SharedByteArrayInputStream(messageContent);
        Date internalDate = Date.from(createdEntry.getValue().getDate().toInstant());
        Flags flags = getMessageFlags(createdEntry.getValue());

        long uid = outbox.appendMessage(content, internalDate, session, flags.contains(Flags.Flag.RECENT), flags);

        return MetaDataWithContent.builder().uid(uid).flags(flags).size(messageContent.length)
                .internalDate(internalDate).sharedContent(content).attachments(messageAttachments)
                .mailboxId(outbox.getId()).messageId(new MessageId(session.getUser(), outbox.getMailboxPath(), uid))
                .build();
    }

    private ImmutableList<MessageAttachment> getMessageAttachments(MailboxSession session,
            ImmutableList<Attachment> attachments) throws MailboxException {
        ThrowingFunction<Attachment, MessageAttachment> toMessageAttachment = att -> messageAttachment(session,
                att);
        return attachments.stream().map(Throwing.function(toMessageAttachment).sneakyThrow())
                .collect(Guavate.toImmutableList());
    }

    private MessageAttachment messageAttachment(MailboxSession session, Attachment attachment)
            throws MailboxException {
        try {
            return MessageAttachment.builder()
                    .attachment(attachmentManager
                            .getAttachment(AttachmentId.from(attachment.getBlobId().getRawValue()), session))
                    .name(attachment.getName().orElse(null)).cid(attachment.getCid().map(Cid::from).orElse(null))
                    .isInline(attachment.isIsInline()).build();
        } catch (AttachmentNotFoundException e) {
            // should not happen (checked before)
            LOG.error(String.format("Attachment %s not found", attachment.getBlobId()), e);
            return null;
        }
    }

    private Flags getMessageFlags(CreationMessage message) {
        Flags result = new Flags();
        if (!message.isIsUnread()) {
            result.add(Flags.Flag.SEEN);
        }
        if (message.isIsFlagged()) {
            result.add(Flags.Flag.FLAGGED);
        }
        if (message.isIsAnswered() || message.getInReplyToMessageId().isPresent()) {
            result.add(Flags.Flag.ANSWERED);
        }
        if (message.isIsDraft()) {
            result.add(Flags.Flag.DRAFT);
        }
        return result;
    }

    private MessageWithId sendMessage(CreationMessageId creationId, MetaDataWithContent message,
            MailboxSession session) throws MailboxException, MessagingException {
        Message jmapMessage = messageFactory.fromMetaDataWithContent(message);
        sendMessage(message, jmapMessage, session);
        return new MessageWithId(creationId, jmapMessage);
    }

    private void sendMessage(MetaDataWithContent message, Message jmapMessage, MailboxSession session)
            throws MessagingException {
        Mail mail = buildMessage(message, jmapMessage);
        try {
            MailMetadata metadata = new MailMetadata(jmapMessage.getId(), session.getUser().getUserName());
            mailSpool.send(mail, metadata);
        } finally {
            LifecycleUtil.dispose(mail);
        }
    }

    private Mail buildMessage(MetaDataWithContent message, Message jmapMessage) throws MessagingException {
        try {
            return mailFactory.build(message, jmapMessage);
        } catch (IOException e) {
            throw new MessagingException("error building message to send", e);
        }
    }
}