blob: 453f932e9ec4a31029a2f59c7b7e8c71e3d61d8c [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.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.function.Supplier;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.james.core.Domain;
import org.apache.james.core.Username;
import org.apache.james.dnsservice.api.DNSService;
import org.apache.james.domainlist.api.DomainListException;
import org.apache.james.domainlist.lib.DomainListConfiguration;
import org.apache.james.domainlist.memory.MemoryDomainList;
import org.apache.james.jmap.draft.exceptions.MailboxNotOwnedException;
import org.apache.james.jmap.draft.model.CreationMessage;
import org.apache.james.jmap.draft.model.CreationMessage.DraftEmailer;
import org.apache.james.jmap.draft.model.CreationMessageId;
import org.apache.james.jmap.draft.model.SetMessagesRequest;
import org.apache.james.jmap.draft.model.SetMessagesResponse;
import org.apache.james.jmap.draft.model.message.view.MessageFullViewFactory;
import org.apache.james.jmap.draft.send.MailMetadata;
import org.apache.james.jmap.draft.send.MailSpool;
import org.apache.james.jmap.draft.utils.JsoupHtmlTextExtractor;
import org.apache.james.jmap.memory.projections.MemoryMessageFastViewProjection;
import org.apache.james.mailbox.AttachmentContentLoader;
import org.apache.james.mailbox.AttachmentManager;
import org.apache.james.mailbox.BlobManager;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MailboxSessionUtil;
import org.apache.james.mailbox.MessageIdManager;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.MessageUid;
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.inmemory.InMemoryId;
import org.apache.james.mailbox.model.ComposedMessageId;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxId.Factory;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.TestMessageId;
import org.apache.james.metrics.tests.RecordingMetricFactory;
import org.apache.james.rrt.api.AliasReverseResolver;
import org.apache.james.rrt.api.CanSendFrom;
import org.apache.james.rrt.api.RecipientRewriteTableConfiguration;
import org.apache.james.rrt.api.RecipientRewriteTableException;
import org.apache.james.rrt.lib.AliasReverseResolverImpl;
import org.apache.james.rrt.lib.CanSendFromImpl;
import org.apache.james.rrt.lib.Mapping;
import org.apache.james.rrt.lib.MappingSource;
import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
import org.apache.james.util.html.HtmlTextExtractor;
import org.apache.james.util.mime.MessageContentExtractor;
import org.apache.mailet.Mail;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import reactor.core.publisher.Flux;
public class SetMessagesCreationProcessorTest {
private static final Username USER = Username.of("user@example.com");
private static final Username OTHER_USER = Username.of("other@example.com");
private static final String OUTBOX = "outbox";
private static final InMemoryId OUTBOX_ID = InMemoryId.of(12345);
private static final String DRAFTS = "drafts";
private static final InMemoryId DRAFTS_ID = InMemoryId.of(12);
private final CreationMessage.Builder creationMessageBuilder = CreationMessage.builder()
.from(DraftEmailer.builder().name("alice").email("alice@example.com").build())
.to(ImmutableList.of(DraftEmailer.builder().name("bob").email("bob@example.com").build()))
.subject("Hey! ");
private final CreationMessageId creationMessageId = CreationMessageId.of("dlkja");
private final SetMessagesRequest createMessageInOutbox = SetMessagesRequest.builder()
.create(
creationMessageId,
creationMessageBuilder
.mailboxId(OUTBOX_ID.serialize())
.from(DraftEmailer.builder().name("user").email("user@example.com").build())
.build())
.build();
private MessageFullViewFactory messageFullViewFactory;
private MailSpool mockedMailSpool;
private SystemMailboxesProvider fakeSystemMailboxesProvider;
private MailboxSession session;
private AttachmentManager mockedAttachmentManager;
private MailboxManager mockedMailboxManager;
private Factory mockedMailboxIdFactory;
private MemoryRecipientRewriteTable recipientRewriteTable;
private CanSendFrom canSendFrom;
private SetMessagesCreationProcessor sut;
private MessageManager outbox;
private MessageManager drafts;
private Optional<MessageManager> optionalOutbox;
private Optional<MessageManager> optionalDrafts;
@Rule
public ExpectedException expectedException = ExpectedException.none();
private MessageAppender messageAppender;
private MessageSender messageSender;
private ReferenceUpdater referenceUpdater;
@Before
public void setUp() throws MailboxException, DomainListException, UnknownHostException, ConfigurationException {
MessageContentExtractor messageContentExtractor = new MessageContentExtractor();
HtmlTextExtractor htmlTextExtractor = new JsoupHtmlTextExtractor();
BlobManager blobManager = mock(BlobManager.class);
when(blobManager.toBlobId(any(MessageId.class))).thenReturn(org.apache.james.mailbox.model.BlobId.fromString("fake"));
MessageIdManager messageIdManager = mock(MessageIdManager.class);
recipientRewriteTable = new MemoryRecipientRewriteTable();
DNSService dnsService = mock(DNSService.class);
MemoryDomainList domainList = new MemoryDomainList(dnsService);
domainList.configure(DomainListConfiguration.DEFAULT);
domainList.addDomain(Domain.of("example.com"));
domainList.addDomain(Domain.of("other.org"));
recipientRewriteTable.setDomainList(domainList);
recipientRewriteTable.setConfiguration(RecipientRewriteTableConfiguration.DEFAULT_ENABLED);
AliasReverseResolver aliasReverseResolver = new AliasReverseResolverImpl(recipientRewriteTable);
canSendFrom = new CanSendFromImpl(recipientRewriteTable, aliasReverseResolver);
messageFullViewFactory = new MessageFullViewFactory(blobManager, messageContentExtractor, htmlTextExtractor,
messageIdManager,
new MemoryMessageFastViewProjection(new RecordingMetricFactory()));
mockedMailSpool = mock(MailSpool.class);
mockedAttachmentManager = mock(AttachmentManager.class);
mockedMailboxManager = mock(MailboxManager.class);
mockedMailboxIdFactory = mock(Factory.class);
MessageIdManager mockMessageIdManager = mock(MessageIdManager.class);
fakeSystemMailboxesProvider = new TestSystemMailboxesProvider(() -> optionalOutbox, () -> optionalDrafts);
session = MailboxSessionUtil.create(USER);
MIMEMessageConverter mimeMessageConverter = new MIMEMessageConverter(mock(AttachmentContentLoader.class));
messageAppender = new MessageAppender(mockedMailboxManager, mockMessageIdManager, mockedAttachmentManager, mimeMessageConverter);
messageSender = new MessageSender(mockedMailSpool);
referenceUpdater = new ReferenceUpdater(mockMessageIdManager, mockedMailboxManager);
sut = new SetMessagesCreationProcessor(messageFullViewFactory,
fakeSystemMailboxesProvider,
new AttachmentChecker(mockedAttachmentManager),
new RecordingMetricFactory(),
mockedMailboxManager,
mockedMailboxIdFactory,
messageAppender,
messageSender,
referenceUpdater,
canSendFrom);
outbox = mock(MessageManager.class);
when(mockedMailboxIdFactory.fromString(OUTBOX_ID.serialize()))
.thenReturn(OUTBOX_ID);
when(mockedMailboxManager.getMailbox(OUTBOX_ID, session))
.thenReturn(outbox);
when(outbox.getId()).thenReturn(OUTBOX_ID);
when(outbox.getMailboxPath()).thenReturn(MailboxPath.forUser(USER, OUTBOX));
when(outbox.appendMessage(any(MessageManager.AppendCommand.class), any(MailboxSession.class)))
.thenReturn(new MessageManager.AppendResult(new ComposedMessageId(OUTBOX_ID, TestMessageId.of(23), MessageUid.of(1)),
Optional.of(ImmutableList.of())));
drafts = mock(MessageManager.class);
when(drafts.getId()).thenReturn(DRAFTS_ID);
when(drafts.getMailboxPath()).thenReturn(MailboxPath.forUser(USER, DRAFTS));
optionalOutbox = Optional.of(outbox);
optionalDrafts = Optional.of(drafts);
}
@Test
public void processShouldReturnEmptyCreatedWhenRequestHasEmptyCreate() {
SetMessagesRequest requestWithEmptyCreate = SetMessagesRequest.builder().build();
SetMessagesResponse result = sut.process(requestWithEmptyCreate, session);
assertThat(result.getCreated()).isEmpty();
assertThat(result.getNotCreated()).isEmpty();
}
@Test
public void processShouldThrowWhenBothIsFlagAndKeywords() {
expectedException.expect(IllegalArgumentException.class);
SetMessagesRequest createMessageWithError = SetMessagesRequest.builder()
.create(
creationMessageId,
creationMessageBuilder
.mailboxId(OUTBOX_ID.serialize())
.isAnswered(Optional.of(true))
.keywords(ImmutableMap.of("$Answered", true))
.build())
.build();
sut.process(createMessageWithError, session);
}
@Test
public void processShouldCreateWhenKeywords() {
SetMessagesRequest createMessageWithKeywords = SetMessagesRequest.builder()
.create(
creationMessageId,
creationMessageBuilder
.mailboxId(OUTBOX_ID.serialize())
.keywords(ImmutableMap.of("$Answered", true))
.build())
.build();
SetMessagesResponse result = sut.process(createMessageWithKeywords, session);
assertThat(result.getCreated()).isNotEmpty();
assertThat(result.getNotCreated()).isEmpty();
}
@Test
public void processShouldCreateWhenIsFlag() {
SetMessagesRequest createMessageWithKeywords = SetMessagesRequest.builder()
.create(
creationMessageId,
creationMessageBuilder
.mailboxId(OUTBOX_ID.serialize())
.isAnswered(Optional.of(true))
.build())
.build();
SetMessagesResponse result = sut.process(createMessageWithKeywords, session);
assertThat(result.getCreated()).isNotEmpty();
assertThat(result.getNotCreated()).isEmpty();
}
@Test
public void processShouldReturnNonEmptyCreatedWhenRequestHasNonEmptyCreate() throws MailboxException {
// Given
sut = new SetMessagesCreationProcessor(messageFullViewFactory, fakeSystemMailboxesProvider, new AttachmentChecker(mockedAttachmentManager), new RecordingMetricFactory(), mockedMailboxManager, mockedMailboxIdFactory, messageAppender, messageSender, referenceUpdater, canSendFrom);
// When
SetMessagesResponse result = sut.process(createMessageInOutbox, session);
// Then
assertThat(result.getCreated()).isNotEmpty();
assertThat(result.getNotCreated()).isEmpty();
}
@Ignore("JAMES-1716 : should report an error")
@Test
public void processShouldReturnErrorWhenOutboxNotFound() {
// Given
TestSystemMailboxesProvider doNotProvideOutbox = new TestSystemMailboxesProvider(Optional::empty, () -> optionalDrafts);
SetMessagesCreationProcessor sut = new SetMessagesCreationProcessor(messageFullViewFactory, doNotProvideOutbox,
new AttachmentChecker(mockedAttachmentManager), new RecordingMetricFactory(), mockedMailboxManager, mockedMailboxIdFactory,
messageAppender,
messageSender,
referenceUpdater,
canSendFrom);
// When
SetMessagesResponse actual = sut.process(createMessageInOutbox, session);
assertThat(actual.getNotCreated()).hasSize(1).containsKey(creationMessageId);
assertThat(actual.getNotCreated().get(creationMessageId).getType()).isEqualTo("invalidProperties");
assertThat(actual.getNotCreated().get(creationMessageId).getDescription()).contains("target mailbox does not exists");
}
@Test
public void processShouldCallAppendMessageWhenRequestHasNonEmptyCreate() throws MailboxException {
// When
sut.process(createMessageInOutbox, session);
// Then
verify(outbox).appendMessage(any(MessageManager.AppendCommand.class), any(MailboxSession.class));
}
@Test
public void processShouldSendMailWhenRequestHasNonEmptyCreate() throws Exception {
// When
sut.process(createMessageInOutbox, session);
// Then
verify(mockedMailSpool).send(any(Mail.class), any(MailMetadata.class));
}
@Test
public void processShouldNotSpoolMailWhenNotSavingToOutbox() throws Exception {
// When
SetMessagesRequest notInOutboxCreationRequest =
SetMessagesRequest.builder()
.create(CreationMessageId.of("anything-really"),
creationMessageBuilder.mailboxId("any-id-but-outbox-id")
.build())
.build();
when(mockedMailboxManager.getMailbox(any(MailboxId.class), any()))
.thenReturn(outbox);
when(mockedMailboxIdFactory.fromString(anyString())).thenReturn(OUTBOX_ID);
sut.process(notInOutboxCreationRequest, session);
// Then
verify(mockedMailSpool, never()).send(any(Mail.class), any(MailMetadata.class));
}
@Test
public void processShouldNotSendWhenSavingToDrafts() throws Exception {
// When
CreationMessageId creationMessageId = CreationMessageId.of("anything-really");
SetMessagesRequest createMessageInDrafts = SetMessagesRequest.builder()
.create(
creationMessageId, creationMessageBuilder.mailboxId(DRAFTS_ID.serialize()).build())
.build();
when(mockedMailboxManager.getMailbox(any(MailboxId.class), any()))
.thenReturn(drafts);
when(mockedMailboxIdFactory.fromString(anyString())).thenReturn(DRAFTS_ID);
sut.process(createMessageInDrafts, session);
// Then
verify(mockedMailSpool, never()).send(any(Mail.class), any(MailMetadata.class));
}
@Test
public void validateIsUserOwnerOfMailboxesShouldThrowWhenMailboxIdDoesntExist() throws Exception {
InMemoryId mailboxId = InMemoryId.of(6789);
when(mockedMailboxManager.getMailbox(mailboxId, session))
.thenThrow(new MailboxNotFoundException(mailboxId));
when(mockedMailboxIdFactory.fromString(mailboxId.serialize()))
.thenReturn(mailboxId);
assertThatThrownBy(() -> sut.assertIsUserOwnerOfMailboxes(ImmutableList.of(mailboxId), session));
}
@Test
public void assertIsUserOwnerOfMailboxesShouldThrowWhenRetrievingMailboxPathFails() throws Exception {
InMemoryId mailboxId = InMemoryId.of(6789);
MessageManager mailbox = mock(MessageManager.class);
when(mockedMailboxManager.getMailbox(mailboxId, session))
.thenReturn(mailbox);
when(mockedMailboxIdFactory.fromString(mailboxId.serialize()))
.thenReturn(mailboxId);
when(mailbox.getMailboxPath())
.thenThrow(new MailboxException());
assertThatThrownBy(() -> sut.assertIsUserOwnerOfMailboxes(ImmutableList.of(mailboxId), session));
}
@Test
public void assertIsUserOwnerOfMailboxesShouldThrowWhenUserIsNotTheOwnerOfTheMailbox() throws Exception {
InMemoryId mailboxId = InMemoryId.of(6789);
MessageManager mailbox = mock(MessageManager.class);
when(mockedMailboxManager.getMailbox(mailboxId, session))
.thenReturn(mailbox);
when(mockedMailboxIdFactory.fromString(mailboxId.serialize()))
.thenReturn(mailboxId);
when(mailbox.getMailboxPath())
.thenReturn(MailboxPath.forUser(Username.of("otheruser@example.com"), mailboxId.serialize()));
assertThatThrownBy(() -> sut.assertIsUserOwnerOfMailboxes(ImmutableList.of(mailboxId), session))
.isInstanceOf(MailboxNotOwnedException.class);
}
@Test
public void assertIsUserOwnerOfMailboxesShouldNotThrowWhenUserIsTheOwnerOfTheMailbox() throws Exception {
InMemoryId mailboxId = InMemoryId.of(6789);
MessageManager mailbox = mock(MessageManager.class);
when(mockedMailboxManager.getMailbox(mailboxId, session))
.thenReturn(mailbox);
when(mockedMailboxIdFactory.fromString(mailboxId.serialize()))
.thenReturn(mailboxId);
when(mailbox.getMailboxPath())
.thenReturn(MailboxPath.forUser(USER, mailboxId.serialize()));
sut.assertIsUserOwnerOfMailboxes(ImmutableList.of(mailboxId), session);
}
@Test
public void assertUserCanSendFromShouldThrowWhenSenderIsNotTheConnectedUser() {
DraftEmailer sender = DraftEmailer
.builder()
.name("other")
.email("other@example.com")
.build();
assertThatThrownBy(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
.isInstanceOf(MailboxSendingNotAllowedException.class);
}
@Test
public void assertUserCanSendFromShouldNotThrowWhenSenderIsTheConnectedUser() {
DraftEmailer sender = DraftEmailer
.builder()
.name("user")
.email(USER.asString())
.build();
assertThatCode(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
.doesNotThrowAnyException();
}
@Test
public void assertUserCanSendFromShouldThrowWhenSenderIsAnAliasOfAnotherUser() throws Exception {
DraftEmailer sender = DraftEmailer
.builder()
.name("alias")
.email("alias@example.com")
.build();
recipientRewriteTable.addAliasMapping(MappingSource.fromUser("alias", "example.com"), OTHER_USER.asString());
assertThatThrownBy(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
.isInstanceOf(MailboxSendingNotAllowedException.class);
}
@Test
public void assertUserCanSendFromShouldNotThrowWhenSenderIsAnAliasOfTheConnectedUser() throws RecipientRewriteTableException {
DraftEmailer sender = DraftEmailer
.builder()
.name("alias")
.email("alias@example.com")
.build();
recipientRewriteTable.addAliasMapping(MappingSource.fromUser("alias", "example.com"), USER.asString());
assertThatCode(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
.doesNotThrowAnyException();
}
@Test
public void assertUserCanSendFromShouldNotThrowWhenSenderIsAnAliasOfTheConnectedUserFromADomainAlias() throws RecipientRewriteTableException {
DraftEmailer sender = DraftEmailer
.builder()
.name("user")
.email("user@other.org")
.build();
recipientRewriteTable.addMapping(MappingSource.fromDomain(Domain.of("other.org")), Mapping.domainAlias(Domain.of("example.com")));
assertThatCode(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
.doesNotThrowAnyException();
}
@Test
public void assertUserCanSendFromShouldThrowWhenSenderIsAnAliasOfTheConnectedUserFromAGroupAlias() throws RecipientRewriteTableException {
DraftEmailer sender = DraftEmailer
.builder()
.name("group")
.email("group@example.com")
.build();
recipientRewriteTable.addGroupMapping(MappingSource.fromUser("group", "example.com"), USER.asString());
assertThatThrownBy(() -> sut.assertUserCanSendFrom(USER, Optional.of(sender)))
.isInstanceOf(MailboxSendingNotAllowedException.class);
}
public static class TestSystemMailboxesProvider implements SystemMailboxesProvider {
private final Supplier<Optional<MessageManager>> outboxSupplier;
private final Supplier<Optional<MessageManager>> draftsSupplier;
private TestSystemMailboxesProvider(Supplier<Optional<MessageManager>> outboxSupplier,
Supplier<Optional<MessageManager>> draftsSupplier) {
this.outboxSupplier = outboxSupplier;
this.draftsSupplier = draftsSupplier;
}
@Override
public Flux<MessageManager> getMailboxByRole(Role aRole, Username username) {
if (aRole.equals(Role.OUTBOX)) {
return Flux.fromStream(outboxSupplier.get().stream());
} else if (aRole.equals(Role.DRAFTS)) {
return Flux.fromStream(draftsSupplier.get().stream());
}
return Flux.empty();
}
}
}