blob: 4b2860744a225f79709d39e8406359a89a8d5b09 [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.model.message.view;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import jakarta.inject.Inject;
import jakarta.mail.internet.SharedInputStream;
import org.apache.james.jmap.api.model.Preview;
import org.apache.james.jmap.api.projections.MessageFastViewPrecomputedProperties;
import org.apache.james.jmap.api.projections.MessageFastViewProjection;
import org.apache.james.jmap.methods.BlobManager;
import org.apache.james.jmap.model.Attachment;
import org.apache.james.jmap.model.BlobId;
import org.apache.james.jmap.model.Emailer;
import org.apache.james.jmap.model.Keywords;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageIdManager;
import org.apache.james.mailbox.MessageUid;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.Cid;
import org.apache.james.mailbox.model.FetchGroup;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MessageAttachmentMetadata;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.MessageResult;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.util.ReactorUtils;
import org.apache.james.util.html.HtmlTextExtractor;
import org.apache.james.util.mime.MessageContentExtractor;
import org.apache.james.util.mime.MessageContentExtractor.MessageContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.fge.lambdas.Throwing;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
public class MessageFullViewFactory implements MessageViewFactory<MessageFullView> {
private static final Logger LOGGER = LoggerFactory.getLogger(MessageFullViewFactory.class);
private final BlobManager blobManager;
private final MessageContentExtractor messageContentExtractor;
private final HtmlTextExtractor htmlTextExtractor;
private final MessageIdManager messageIdManager;
private final MessageFastViewProjection fastViewProjection;
@Inject
public MessageFullViewFactory(BlobManager blobManager, MessageContentExtractor messageContentExtractor,
HtmlTextExtractor htmlTextExtractor, MessageIdManager messageIdManager,
MessageFastViewProjection fastViewProjection) {
this.blobManager = blobManager;
this.messageContentExtractor = messageContentExtractor;
this.htmlTextExtractor = htmlTextExtractor;
this.messageIdManager = messageIdManager;
this.fastViewProjection = fastViewProjection;
}
@Override
public Flux<MessageFullView> fromMessageIds(List<MessageId> messageIds, MailboxSession mailboxSession) {
Flux<MessageResult> messages = Flux.from(messageIdManager.getMessagesReactive(messageIds, FetchGroup.FULL_CONTENT, mailboxSession));
return Helpers.toMessageViews(messages, this::fromMessageResults);
}
public Mono<MessageFullView> fromMetaDataWithContent(MetaDataWithContent message) {
return Mono.fromCallable(() -> Helpers.retrieveMessage(message))
.flatMap(Throwing.function(mimeMessage -> fromMetaDataWithContent(message, mimeMessage)));
}
private Mono<MessageFullView> fromMetaDataWithContent(MetaDataWithContent message, Message mimeMessage) throws IOException {
MessageContent messageContent = messageContentExtractor.extract(mimeMessage);
Optional<String> htmlBody = messageContent.getHtmlBody();
Optional<String> textBody = computeTextBodyIfNeeded(messageContent);
return retrieveProjection(messageContent, message.getMessageId(),
() -> MessageFullView.hasAttachment(getAttachments(message.getAttachments())))
.map(messageProjection -> instanciateMessageFullView(message, mimeMessage, htmlBody, textBody, messageProjection));
}
private MessageFullView instanciateMessageFullView(MetaDataWithContent message, Message mimeMessage, Optional<String> htmlBody, Optional<String> textBody, MessageFastViewPrecomputedProperties messageProjection) {
return MessageFullView.builder()
.id(message.getMessageId())
.blobId(BlobId.of(message.getMessageId()))
.threadId(message.getMessageId().serialize())
.mailboxIds(message.getMailboxIds())
.inReplyToMessageId(Helpers.getHeaderValue(mimeMessage, "in-reply-to"))
.keywords(message.getKeywords())
.subject(Strings.nullToEmpty(mimeMessage.getSubject()).trim())
.headers(Helpers.toHeaderMap(mimeMessage.getHeader()))
.from(Emailer.firstFromMailboxList(mimeMessage.getFrom()))
.to(Emailer.fromAddressList(mimeMessage.getTo()))
.cc(Emailer.fromAddressList(mimeMessage.getCc()))
.bcc(Emailer.fromAddressList(mimeMessage.getBcc()))
.replyTo(Emailer.fromAddressList(mimeMessage.getReplyTo()))
.size(message.getSize())
.date(getDateFromHeaderOrInternalDateOtherwise(mimeMessage, message))
.textBody(textBody)
.htmlBody(htmlBody)
.preview(messageProjection.getPreview())
.attachments(getAttachments(message.getAttachments()))
.build();
}
private Mono<MessageFastViewPrecomputedProperties> retrieveProjection(MessageContent messageContent,
MessageId messageId, Supplier<Boolean> hasAttachments) {
return Mono.from(fastViewProjection.retrieve(messageId))
.onErrorResume(throwable -> fallBackToCompute(messageContent, hasAttachments, throwable))
.switchIfEmpty(computeThenStoreAsync(messageContent, messageId, hasAttachments));
}
private Mono<MessageFastViewPrecomputedProperties> fallBackToCompute(MessageContent messageContent,
Supplier<Boolean> hasAttachments,
Throwable throwable) {
LOGGER.error("Cannot retrieve the computed preview from MessageFastViewProjection", throwable);
return computeProjection(messageContent, hasAttachments);
}
private Mono<MessageFastViewPrecomputedProperties> computeThenStoreAsync(MessageContent messageContent,
MessageId messageId,
Supplier<Boolean> hasAttachments) {
return computeProjection(messageContent, hasAttachments)
.doOnNext(projection -> Mono.from(fastViewProjection.store(messageId, projection))
.doOnError(throwable -> LOGGER.error("Cannot store the projection to MessageFastViewProjection", throwable))
.subscribeOn(Schedulers.parallel())
.subscribe());
}
private Mono<MessageFastViewPrecomputedProperties> computeProjection(MessageContent messageContent, Supplier<Boolean> hasAttachments) {
return Mono.fromCallable(() -> mainTextContent(messageContent))
.handle(ReactorUtils.publishIfPresent())
.map(Preview::compute)
.defaultIfEmpty(Preview.EMPTY)
.map(extractedPreview -> MessageFastViewPrecomputedProperties.builder()
.preview(extractedPreview)
.hasAttachment(hasAttachments.get())
.build());
}
private Instant getDateFromHeaderOrInternalDateOtherwise(Message mimeMessage, MessageFullViewFactory.MetaDataWithContent message) {
return Optional.ofNullable(mimeMessage.getDate())
.map(Date::toInstant)
.orElse(message.getInternalDate());
}
Mono<MessageFullView> fromMessageResults(Collection<MessageResult> messageResults) {
try {
return fromMetaDataWithContent(toMetaDataWithContent(messageResults));
} catch (MailboxException e) {
return Mono.error(e);
}
}
private MetaDataWithContent toMetaDataWithContent(Collection<MessageResult> messageResults) throws MailboxException {
Helpers.assertOneMessageId(messageResults);
MessageResult firstMessageResult = messageResults.iterator().next();
Set<MailboxId> mailboxIds = Helpers.getMailboxIds(messageResults);
Keywords keywords = Helpers.getKeywords(messageResults);
return MetaDataWithContent.builderFromMessageResult(firstMessageResult)
.messageId(firstMessageResult.getMessageId())
.mailboxIds(mailboxIds)
.keywords(keywords)
.build();
}
private Optional<String> computeTextBodyIfNeeded(MessageContent messageContent) {
return messageContent.getTextBody()
.or(() -> messageContent.extractMainTextContent(htmlTextExtractor));
}
private Optional<String> mainTextContent(MessageContent messageContent) {
return messageContent.getHtmlBody()
.map(htmlTextExtractor::toPlainText)
.filter(Predicate.not(Strings::isNullOrEmpty))
.or(messageContent::getTextBody);
}
private List<Attachment> getAttachments(List<MessageAttachmentMetadata> attachments) {
return attachments.stream()
.map(this::fromMailboxAttachment)
.collect(ImmutableList.toImmutableList());
}
private Attachment fromMailboxAttachment(MessageAttachmentMetadata attachment) {
return Attachment.builder()
.blobId(BlobId.of(attachment.getAttachmentId()))
.type(attachment.getAttachment().getType())
.size(attachment.getAttachment().getSize())
.name(attachment.getName())
.cid(attachment.getCid().map(Cid::getValue))
.isInline(attachment.isInline())
.build();
}
public static class MetaDataWithContent {
public static Builder builder() {
return new Builder();
}
public static Builder builderFromMessageResult(MessageResult messageResult) throws MailboxException {
Builder builder = builder()
.uid(messageResult.getUid())
.size(messageResult.getSize())
.internalDate(messageResult.getInternalDate().toInstant())
.attachments(messageResult.getLoadedAttachments())
.mailboxId(messageResult.getMailboxId());
try {
return builder.content(messageResult.getFullContent().getInputStream());
} catch (IOException e) {
throw new MailboxException("Can't get message full content: " + e.getMessage(), e);
}
}
public static class Builder {
private MessageUid uid;
private Keywords keywords;
private Long size;
private Instant internalDate;
private InputStream content;
private SharedInputStream sharedContent;
private List<MessageAttachmentMetadata> attachments;
private Set<MailboxId> mailboxIds = Sets.newHashSet();
private MessageId messageId;
private Optional<Message> message;
public Builder() {
this.message = Optional.empty();
}
public Builder uid(MessageUid uid) {
this.uid = uid;
return this;
}
public Builder keywords(Keywords keywords) {
this.keywords = keywords;
return this;
}
public Builder size(long size) {
this.size = size;
return this;
}
public Builder internalDate(Instant internalDate) {
this.internalDate = internalDate;
return this;
}
public Builder content(InputStream content) {
this.content = content;
return this;
}
public Builder sharedContent(SharedInputStream sharedContent) {
this.sharedContent = sharedContent;
return this;
}
public Builder attachments(List<MessageAttachmentMetadata> attachments) {
this.attachments = attachments;
return this;
}
public Builder mailboxId(MailboxId mailboxId) {
this.mailboxIds.add(mailboxId);
return this;
}
public Builder message(Message message) {
this.message = Optional.of(message);
return this;
}
public Builder mailboxIds(Set<MailboxId> mailboxIds) {
this.mailboxIds.addAll(mailboxIds);
return this;
}
public Builder messageId(MessageId messageId) {
this.messageId = messageId;
return this;
}
public MetaDataWithContent build() {
Preconditions.checkArgument(uid != null);
Preconditions.checkArgument(keywords != null);
Preconditions.checkArgument(size != null);
Preconditions.checkArgument(internalDate != null);
Preconditions.checkArgument(content != null ^ sharedContent != null);
Preconditions.checkArgument(attachments != null);
Preconditions.checkArgument(mailboxIds != null);
Preconditions.checkArgument(messageId != null);
return new MetaDataWithContent(uid, keywords, size, internalDate, content, sharedContent, attachments, mailboxIds, messageId, message);
}
}
private final MessageUid uid;
private final Keywords keywords;
private final long size;
private final Instant internalDate;
private final InputStream content;
private final SharedInputStream sharedContent;
private final List<MessageAttachmentMetadata> attachments;
private final Set<MailboxId> mailboxIds;
private final MessageId messageId;
private final Optional<Message> message;
private MetaDataWithContent(MessageUid uid,
Keywords keywords,
long size,
Instant internalDate,
InputStream content,
SharedInputStream sharedContent,
List<MessageAttachmentMetadata> attachments,
Set<MailboxId> mailboxIds,
MessageId messageId, Optional<Message> message) {
this.uid = uid;
this.keywords = keywords;
this.size = size;
this.internalDate = internalDate;
this.content = content;
this.sharedContent = sharedContent;
this.attachments = attachments;
this.mailboxIds = mailboxIds;
this.messageId = messageId;
this.message = message;
}
public Optional<Message> getMessage() {
return message;
}
public MessageUid getUid() {
return uid;
}
public Keywords getKeywords() {
return keywords;
}
public long getSize() {
return size;
}
public Instant getInternalDate() {
return internalDate;
}
public InputStream getContent() {
if (sharedContent != null) {
long begin = 0;
long allContent = -1;
return sharedContent.newStream(begin, allContent);
}
return content;
}
public List<MessageAttachmentMetadata> getAttachments() {
return attachments;
}
public Set<MailboxId> getMailboxIds() {
return mailboxIds;
}
public MessageId getMessageId() {
return messageId;
}
}
}