blob: c32b8e6a6072647ad76a55330e029ac9e3eb7f8b [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.model;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import org.apache.james.jmap.draft.methods.ValidationResult;
import org.apache.james.jmap.model.Attachment;
import org.apache.james.jmap.model.BlobId;
import org.apache.james.jmap.model.Keyword;
import org.apache.james.jmap.model.Keywords;
import org.apache.james.jmap.model.MessageProperties.MessageProperty;
import org.apache.james.jmap.model.message.view.SubMessage;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.util.StreamUtils;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@JsonDeserialize(builder = CreationMessage.Builder.class)
public class CreationMessage {
private static final String RECIPIENT_PROPERTY_NAMES = ImmutableList.of(MessageProperty.to, MessageProperty.cc, MessageProperty.bcc).stream()
.map(MessageProperty::asFieldName)
.collect(Collectors.joining(", "));
public static Builder builder() {
return new Builder();
}
@JsonPOJOBuilder(withPrefix = "")
public static class Builder {
private ImmutableList<String> mailboxIds;
private String inReplyToMessageId;
private final OldKeyword.Builder oldKeywordBuilder;
private final ImmutableMap.Builder<String, String> headers;
private Optional<DraftEmailer> from = Optional.empty();
private final ImmutableList.Builder<DraftEmailer> to;
private final ImmutableList.Builder<DraftEmailer> cc;
private final ImmutableList.Builder<DraftEmailer> bcc;
private final ImmutableList.Builder<DraftEmailer> replyTo;
private String subject;
private ZonedDateTime date;
private String textBody;
private String htmlBody;
private final ImmutableList.Builder<Attachment> attachments;
private final ImmutableMap.Builder<BlobId, SubMessage> attachedMessages;
private Optional<Map<String, Boolean>> keywords = Optional.empty();
private Builder() {
to = ImmutableList.builder();
cc = ImmutableList.builder();
bcc = ImmutableList.builder();
replyTo = ImmutableList.builder();
attachments = ImmutableList.builder();
attachedMessages = ImmutableMap.builder();
headers = ImmutableMap.builder();
oldKeywordBuilder = OldKeyword.builder();
}
public Builder mailboxId(String... mailboxIds) {
return mailboxIds(Arrays.asList(mailboxIds));
}
@JsonDeserialize
public Builder mailboxIds(List<String> mailboxIds) {
this.mailboxIds = ImmutableList.copyOf(mailboxIds);
return this;
}
public Builder inReplyToMessageId(String inReplyToMessageId) {
this.inReplyToMessageId = inReplyToMessageId;
return this;
}
public Builder isUnread(Optional<Boolean> isUnread) {
oldKeywordBuilder.isUnread(isUnread);
return this;
}
public Builder isFlagged(Optional<Boolean> isFlagged) {
oldKeywordBuilder.isFlagged(isFlagged);
return this;
}
public Builder isAnswered(Optional<Boolean> isAnswered) {
oldKeywordBuilder.isAnswered(isAnswered);
return this;
}
public Builder isDraft(Optional<Boolean> isDraft) {
oldKeywordBuilder.isDraft(isDraft);
return this;
}
public Builder isForwarded(Optional<Boolean> isForwarded) {
oldKeywordBuilder.isForwarded(isForwarded);
return this;
}
public Builder headers(ImmutableMap<String, String> headers) {
this.headers.putAll(headers);
return this;
}
public Builder from(DraftEmailer from) {
this.from = Optional.ofNullable(from);
return this;
}
public Builder to(List<DraftEmailer> to) {
this.to.addAll(to);
return this;
}
public Builder cc(List<DraftEmailer> cc) {
this.cc.addAll(cc);
return this;
}
public Builder bcc(List<DraftEmailer> bcc) {
this.bcc.addAll(bcc);
return this;
}
public Builder replyTo(List<DraftEmailer> replyTo) {
this.replyTo.addAll(replyTo);
return this;
}
public Builder subject(String subject) {
this.subject = Strings.nullToEmpty(subject);
return this;
}
public Builder date(ZonedDateTime date) {
this.date = date;
return this;
}
public Builder textBody(String textBody) {
this.textBody = textBody;
return this;
}
public Builder htmlBody(String htmlBody) {
this.htmlBody = htmlBody;
return this;
}
public Builder attachments(Attachment... attachments) {
return attachments(Arrays.asList(attachments));
}
@JsonDeserialize
public Builder attachments(List<Attachment> attachments) {
this.attachments.addAll(attachments);
return this;
}
public Builder attachedMessages(Map<BlobId, SubMessage> attachedMessages) {
this.attachedMessages.putAll(attachedMessages);
return this;
}
public Builder keywords(Map<String, Boolean> keywords) {
this.keywords = Optional.of(ImmutableMap.copyOf(keywords));
return this;
}
private static boolean areAttachedMessagesKeysInAttachments(ImmutableList<Attachment> attachments, ImmutableMap<BlobId, SubMessage> attachedMessages) {
return attachedMessages.isEmpty() || attachedMessages.keySet().stream()
.anyMatch(inAttachments(attachments));
}
private static Predicate<BlobId> inAttachments(ImmutableList<Attachment> attachments) {
return (key) -> attachments.stream()
.map(Attachment::getBlobId)
.anyMatch(blobId -> blobId.equals(key));
}
public CreationMessage build() {
Preconditions.checkState(mailboxIds != null, "'mailboxIds' is mandatory");
Preconditions.checkState(headers != null, "'headers' is mandatory");
ImmutableList<Attachment> attachments = this.attachments.build();
ImmutableMap<BlobId, SubMessage> attachedMessages = this.attachedMessages.build();
Preconditions.checkState(areAttachedMessagesKeysInAttachments(attachments, attachedMessages), "'attachedMessages' keys must be in 'attachments'");
if (date == null) {
date = ZonedDateTime.now();
}
Optional<Keywords> maybeKeywords = creationKeywords();
Optional<OldKeyword> oldKeywords = oldKeywordBuilder.computeOldKeyword();
return new CreationMessage(mailboxIds, Optional.ofNullable(inReplyToMessageId), headers.build(), from,
to.build(), cc.build(), bcc.build(), replyTo.build(), subject, date, Optional.ofNullable(textBody), Optional.ofNullable(htmlBody),
attachments, attachedMessages, computeKeywords(maybeKeywords, oldKeywords));
}
public Optional<Keywords> creationKeywords() {
return keywords.map(map -> Keywords.strictFactory()
.fromMap(map));
}
public Keywords computeKeywords(Optional<Keywords> keywords, Optional<OldKeyword> oldKeywords) {
Preconditions.checkArgument(!(keywords.isPresent() && oldKeywords.isPresent()), "Does not support keyword and is* at the same time");
return keywords
.or(() -> oldKeywords.map(OldKeyword::asKeywords))
.orElse(Keywords.DEFAULT_VALUE);
}
}
private final ImmutableList<String> mailboxIds;
private final Optional<String> inReplyToMessageId;
private final ImmutableMap<String, String> headers;
private final Optional<DraftEmailer> from;
private final ImmutableList<DraftEmailer> to;
private final ImmutableList<DraftEmailer> cc;
private final ImmutableList<DraftEmailer> bcc;
private final ImmutableList<DraftEmailer> replyTo;
private final String subject;
private final ZonedDateTime date;
private final Optional<String> textBody;
private final Optional<String> htmlBody;
private final ImmutableList<Attachment> attachments;
private final ImmutableMap<BlobId, SubMessage> attachedMessages;
private final Keywords keywords;
@VisibleForTesting
CreationMessage(ImmutableList<String> mailboxIds, Optional<String> inReplyToMessageId, ImmutableMap<String, String> headers, Optional<DraftEmailer> from,
ImmutableList<DraftEmailer> to, ImmutableList<DraftEmailer> cc, ImmutableList<DraftEmailer> bcc, ImmutableList<DraftEmailer> replyTo, String subject, ZonedDateTime date, Optional<String> textBody, Optional<String> htmlBody, ImmutableList<Attachment> attachments,
ImmutableMap<BlobId, SubMessage> attachedMessages, Keywords keywords) {
this.mailboxIds = mailboxIds;
this.inReplyToMessageId = inReplyToMessageId;
this.headers = headers;
this.from = from;
this.to = to;
this.cc = cc;
this.bcc = bcc;
this.replyTo = replyTo;
this.subject = subject;
this.date = date;
this.textBody = textBody;
this.htmlBody = htmlBody;
this.attachments = attachments;
this.attachedMessages = attachedMessages;
this.keywords = keywords;
}
public Keywords getKeywords() {
return keywords;
}
public ImmutableList<String> getMailboxIds() {
return mailboxIds;
}
public Optional<String> getInReplyToMessageId() {
return inReplyToMessageId;
}
public ImmutableMap<String, String> getHeaders() {
return headers;
}
public Optional<DraftEmailer> getFrom() {
return from;
}
public ImmutableList<DraftEmailer> getTo() {
return to;
}
public ImmutableList<DraftEmailer> getCc() {
return cc;
}
public ImmutableList<DraftEmailer> getBcc() {
return bcc;
}
public ImmutableList<DraftEmailer> getReplyTo() {
return replyTo;
}
public String getSubject() {
return subject;
}
public ZonedDateTime getDate() {
return date;
}
public Optional<String> getTextBody() {
return textBody;
}
public Optional<String> getHtmlBody() {
return htmlBody;
}
public ImmutableList<Attachment> getAttachments() {
return attachments;
}
public ImmutableMap<BlobId, SubMessage> getAttachedMessages() {
return attachedMessages;
}
public boolean isValid() {
return validate().isEmpty();
}
public boolean isDraft() {
return keywords.contains(Keyword.DRAFT);
}
public List<ValidationResult> validate() {
ImmutableList.Builder<ValidationResult> errors = ImmutableList.builder();
assertValidFromProvided(errors);
assertAtLeastOneValidRecipient(errors);
return errors.build();
}
private void assertAtLeastOneValidRecipient(ImmutableList.Builder<ValidationResult> errors) {
Stream<DraftEmailer> recipients = StreamUtils.flatten(to.stream(), cc.stream(), bcc.stream());
boolean hasAtLeastOneAddressToSendTo = recipients.anyMatch(DraftEmailer::hasValidEmail);
if (!hasAtLeastOneAddressToSendTo) {
errors.add(ValidationResult.builder().message("no recipient address set").property(RECIPIENT_PROPERTY_NAMES).build());
}
}
private void assertValidFromProvided(ImmutableList.Builder<ValidationResult> errors) {
ValidationResult invalidPropertyFrom = ValidationResult.builder()
.property(MessageProperty.from.asFieldName())
.message("'from' address is mandatory")
.build();
if (!from.isPresent()) {
errors.add(invalidPropertyFrom);
}
from.filter(f -> !f.hasValidEmail()).ifPresent(f -> errors.add(invalidPropertyFrom));
}
public boolean isIn(MessageManager mailbox) {
return mailboxIds.contains(mailbox.getId().serialize());
}
public boolean isOnlyIn(MessageManager mailbox) {
return isIn(mailbox)
&& mailboxIds.size() == 1;
}
@JsonDeserialize(builder = DraftEmailer.Builder.class)
public static class DraftEmailer {
public static Builder builder() {
return new Builder();
}
@JsonPOJOBuilder(withPrefix = "")
public static class Builder {
private Optional<String> name = Optional.empty();
private Optional<String> email = Optional.empty();
public Builder name(String name) {
this.name = Optional.ofNullable(name);
return this;
}
public Builder email(String email) {
this.email = Optional.ofNullable(email);
return this;
}
public DraftEmailer build() {
return new DraftEmailer(name, email);
}
}
private final Optional<String> name;
private final Optional<String> email;
private final EmailValidator emailValidator;
@VisibleForTesting
DraftEmailer(Optional<String> name, Optional<String> email) {
this.name = name;
this.email = email;
this.emailValidator = new EmailValidator();
}
public Optional<String> getName() {
return name;
}
public Optional<String> getEmail() {
return email;
}
public boolean hasValidEmail() {
return getEmail().isPresent() && emailValidator.isValid(getEmail().get());
}
public EmailUserAndDomain getEmailUserAndDomain() {
int atIndex = email.get().indexOf('@');
if (atIndex < 0 || atIndex == email.get().length() - 1) {
return new EmailUserAndDomain(Optional.of(email.get()), Optional.empty());
}
String user = email.get().substring(0, atIndex);
String domain = email.get().substring(atIndex + 1);
return new EmailUserAndDomain(Optional.of(user), Optional.of(domain));
}
@Override
public boolean equals(Object o) {
if (o instanceof DraftEmailer) {
DraftEmailer otherEMailer = (DraftEmailer) o;
return Objects.equals(name, otherEMailer.name)
&& Objects.equals(email.orElse("<unset>"), otherEMailer.email.orElse("<unset>"));
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(name, email);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("email", email.orElse("<unset>"))
.toString();
}
}
public static class EmailUserAndDomain {
private final Optional<String> user;
private final Optional<String> domain;
public EmailUserAndDomain(Optional<String> user, Optional<String> domain) {
this.user = user;
this.domain = domain;
}
public Optional<String> getUser() {
return user;
}
public Optional<String> getDomain() {
return domain;
}
}
public static class EmailValidator {
public boolean isValid(String email) {
boolean result = true;
try {
InternetAddress emailAddress = new InternetAddress(email);
// verrrry permissive validator !
emailAddress.validate();
} catch (AddressException ex) {
result = false;
}
return result;
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("mailboxIds", mailboxIds)
.toString();
}
}