blob: b256e9d1ab249f50bd619caa2d5fdf1717025b1e [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;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
public class MessageProperties {
public static final ImmutableSet<MessageProperty> MANDATORY_PROPERTIES = ImmutableSet.of(MessageProperty.id);
private final Optional<ImmutableSet<MessageProperty>> messageProperties;
private final Optional<ImmutableSet<HeaderProperty>> headersProperties;
public MessageProperties(Optional<ImmutableSet<String>> properties) {
this.messageProperties = properties.map(this::toMessageProperties);
this.headersProperties = properties.map(this::toHeadersProperties);
}
private MessageProperties(Optional<ImmutableSet<MessageProperty>> messageProperties,
Optional<ImmutableSet<HeaderProperty>> headersProperties) {
this.messageProperties = messageProperties;
this.headersProperties = headersProperties;
}
private ImmutableSet<MessageProperty> toMessageProperties(ImmutableSet<String> properties) {
return properties.stream().flatMap(MessageProperty::find).collect(ImmutableSet.toImmutableSet());
}
private ImmutableSet<HeaderProperty> toHeadersProperties(ImmutableSet<String> properties) {
return properties.stream().flatMap(HeaderProperty::find).collect(ImmutableSet.toImmutableSet());
}
public Optional<ImmutableSet<HeaderProperty>> getOptionalHeadersProperties() {
return headersProperties;
}
public Optional<ImmutableSet<MessageProperty>> getOptionalMessageProperties() {
return messageProperties;
}
public MessageProperties toOutputProperties() {
return this.ensureContains(MANDATORY_PROPERTIES)
.selectBody()
.overrideHeadersFilteringOnHeadersMessageProperty()
.ensureHeadersMessageProperty();
}
public ReadProfile computeReadLevel() {
Stream<ReadProfile> readLevels = Stream.concat(this.buildOutputMessageProperties()
.stream()
.map(MessageProperty::getReadProfile),
headerPropertiesReadLevel());
// If `null`, all properties will be fetched
// This defer from RFC-8621 behavior (not implemented here)
// If omitted, this defaults to: [ "partId", "blobId", "size", "name", "type", "charset", "disposition", "cid",
// "language", "location" ]
return readLevels.reduce(ReadProfile::combine)
.orElse(ReadProfile.Full);
}
public Stream<String> asFieldList() {
return Stream.concat(
messageProperties.stream()
.flatMap(Collection::stream)
.map(MessageProperty::asFieldName),
headersProperties.stream()
.flatMap(Collection::stream)
.map(HeaderProperty::asFieldName));
}
private Stream<ReadProfile> headerPropertiesReadLevel() {
return headersProperties.map(collection ->
collection.stream()
.map(any -> ReadProfile.Header))
.orElse(Stream.of());
}
private ImmutableSet<MessageProperty> buildOutputMessageProperties() {
return this.messageProperties.orElseGet(MessageProperty::allOutputProperties);
}
private MessageProperties usingProperties(Sets.SetView<MessageProperty> properties) {
return new MessageProperties(
Optional.of(properties.immutableCopy()),
headersProperties);
}
private MessageProperties ensureContains(ImmutableSet<MessageProperty> mandatoryFields) {
return usingProperties(Sets.union(buildOutputMessageProperties(), mandatoryFields));
}
private MessageProperties selectBody() {
ImmutableSet<MessageProperty> messageProperties = buildOutputMessageProperties();
if (messageProperties.contains(MessageProperty.body)) {
return usingProperties(
Sets.difference(
Sets.union(messageProperties, ImmutableSet.of(MessageProperty.textBody)),
ImmutableSet.of(MessageProperty.body)));
}
return this;
}
private MessageProperties ensureHeadersMessageProperty() {
if (headersProperties.isPresent() && !headersProperties.get().isEmpty()) {
return usingProperties(Sets.union(
buildOutputMessageProperties(),
ImmutableSet.of(MessageProperty.headers)));
}
return this;
}
private MessageProperties overrideHeadersFilteringOnHeadersMessageProperty() {
if (buildOutputMessageProperties().contains(MessageProperty.headers)) {
return new MessageProperties(messageProperties, Optional.empty());
}
return this;
}
private enum PropertyType {
INPUTONLY,
INPUTOUTPUT
}
public enum MessageProperty implements Property {
id("id", ReadProfile.Metadata),
blobId("blobId", ReadProfile.Metadata),
threadId("threadId", ReadProfile.Metadata),
mailboxIds("mailboxIds", ReadProfile.Metadata),
inReplyToMessageId("inReplyToMessageId", ReadProfile.Header),
isUnread("isUnread", ReadProfile.Metadata),
isFlagged("isFlagged", ReadProfile.Metadata),
isAnswered("isAnswered", ReadProfile.Metadata),
isDraft("isDraft", ReadProfile.Metadata),
isForwarded("isForwarded", ReadProfile.Metadata),
hasAttachment("hasAttachment", ReadProfile.Fast),
headers("headers", ReadProfile.Header),
from("from", ReadProfile.Header),
to("to", ReadProfile.Header),
cc("cc", ReadProfile.Header),
bcc("bcc", ReadProfile.Header),
replyTo("replyTo", ReadProfile.Header),
subject("subject", ReadProfile.Header),
date("date", ReadProfile.Header),
size("size", ReadProfile.Metadata),
preview("preview", ReadProfile.Fast),
textBody("textBody", ReadProfile.Full),
htmlBody("htmlBody", ReadProfile.Full),
attachments("attachments", ReadProfile.Full),
attachedMessages("attachedMessages", ReadProfile.Full),
keywords("keywords", ReadProfile.Metadata),
body("body", PropertyType.INPUTONLY, ReadProfile.Full);
private final String property;
private final PropertyType type;
private final ReadProfile readProfile;
MessageProperty(String property, ReadProfile readProfile) {
this(property, PropertyType.INPUTOUTPUT, readProfile);
}
MessageProperty(String property, PropertyType type, ReadProfile readProfile) {
this.property = property;
this.type = type;
this.readProfile = readProfile;
}
@Override
public String asFieldName() {
return property;
}
public ReadProfile getReadProfile() {
return readProfile;
}
private static final ImmutableMap<String, MessageProperty> LOOKUP_MAP = Arrays.stream(values())
.collect(ImmutableMap.toImmutableMap(v -> v.property, Function.identity()));
public static Stream<MessageProperty> find(String property) {
Preconditions.checkNotNull(property);
return Optional.ofNullable(LOOKUP_MAP.get(property)).stream();
}
public static ImmutableSet<MessageProperty> allOutputProperties() {
return Arrays.stream(values()).filter(MessageProperty::outputProperty).collect(ImmutableSet.toImmutableSet());
}
private static boolean outputProperty(MessageProperty p) {
switch (p.type) {
case INPUTONLY:
return false;
case INPUTOUTPUT:
return true;
default:
throw new IllegalStateException();
}
}
}
public enum ReadProfile {
Metadata(0),
Header(1),
Fast(2),
Full(3);
public static ReadProfile combine(ReadProfile readProfile1, ReadProfile readProfile2) {
if (readProfile1.priority > readProfile2.priority) {
return readProfile1;
}
return readProfile2;
}
private final int priority;
ReadProfile(int priority) {
this.priority = priority;
}
}
public static class HeaderProperty implements Property {
public static final String HEADER_PROPERTY_PREFIX = "headers.";
public static HeaderProperty fromFieldName(String field) {
Preconditions.checkArgument(!isMessageHeaderProperty(field));
return new HeaderProperty(field.toLowerCase(Locale.US));
}
public static HeaderProperty valueOf(String property) {
Preconditions.checkArgument(isMessageHeaderProperty(property));
return new HeaderProperty(stripPrefix(property).toLowerCase(Locale.US));
}
private static String stripPrefix(String property) {
return property.substring(HEADER_PROPERTY_PREFIX.length());
}
public static boolean isMessageHeaderProperty(String property) {
Preconditions.checkNotNull(property);
return property.startsWith(HEADER_PROPERTY_PREFIX);
}
public static Stream<HeaderProperty> find(String property) {
if (isMessageHeaderProperty(property)) {
return Stream.of(valueOf(property));
} else {
return Stream.of();
}
}
private String fieldName;
private HeaderProperty(String fieldName) {
this.fieldName = fieldName;
}
@Override
public String asFieldName() {
return fieldName;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof HeaderProperty) {
HeaderProperty other = (HeaderProperty) obj;
return Objects.equals(this.fieldName, other.fieldName);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(fieldName);
}
@Override
public String toString() {
return Objects.toString(fieldName);
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("headersProperties", headersProperties)
.add("messageProperties", messageProperties)
.toString();
}
}