JAMES-1692 add SetMessagesCreationProcessor impl. for sending message

add CreationMessage to hold new messages.
build up MIME Messages for james to append to OUTBOX
extract SetMessagesProcessor<Id> interface


Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/99ac325d
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/99ac325d
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/99ac325d

Branch: refs/heads/master
Commit: 99ac325d5f9bab58363a62b50a1ff7965611473b
Parents: 0625fb5
Author: Fabien Vignon <[email protected]>
Authored: Fri Feb 12 19:40:12 2016 +0100
Committer: Raphael Ouazana <[email protected]>
Committed: Tue Mar 1 15:42:53 2016 +0100

----------------------------------------------------------------------
 .../org/apache/james/jmap/MethodsModule.java    |   2 +
 .../jmap/methods/SetMessagesMethodTest.java     |   2 -
 .../MailboxRoleNotFoundException.java           |  36 +++
 .../jmap/methods/MIMEMessageConverter.java      | 150 +++++++++
 .../james/jmap/methods/MessageWithId.java       |  48 +++
 .../methods/SetMessagesCreationProcessor.java   | 174 +++++++++++
 .../james/jmap/methods/SetMessagesMethod.java   |   9 +-
 .../jmap/methods/SetMessagesProcessor.java      |  29 ++
 .../methods/SetMessagesUpdateProcessor.java     |   4 +-
 .../james/jmap/model/CreationMessage.java       | 305 +++++++++++++++++++
 .../james/jmap/model/SetMessagesRequest.java    |  12 +-
 .../james/jmap/model/SetMessagesResponse.java   |  27 +-
 .../SetMessagesCreationProcessorTest.java       | 131 ++++++++
 .../jmap/model/SetMessagesResponseTest.java     |  11 +-
 14 files changed, 913 insertions(+), 27 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/container/cassandra-guice/src/main/java/org/apache/james/jmap/MethodsModule.java
----------------------------------------------------------------------
diff --git 
a/server/container/cassandra-guice/src/main/java/org/apache/james/jmap/MethodsModule.java
 
b/server/container/cassandra-guice/src/main/java/org/apache/james/jmap/MethodsModule.java
index 27faf5a..4e57d20 100644
--- 
a/server/container/cassandra-guice/src/main/java/org/apache/james/jmap/MethodsModule.java
+++ 
b/server/container/cassandra-guice/src/main/java/org/apache/james/jmap/MethodsModule.java
@@ -28,6 +28,7 @@ import org.apache.james.jmap.methods.JmapRequestParserImpl;
 import org.apache.james.jmap.methods.JmapResponseWriter;
 import org.apache.james.jmap.methods.JmapResponseWriterImpl;
 import org.apache.james.jmap.methods.Method;
+import org.apache.james.jmap.methods.SetMessagesCreationProcessor;
 import org.apache.james.jmap.methods.SetMessagesMethod;
 import org.apache.james.jmap.methods.SetMessagesUpdateProcessor;
 import org.apache.james.mailbox.cassandra.CassandraId;
@@ -54,6 +55,7 @@ public class MethodsModule extends AbstractModule {
         methods.addBinding().to(new 
TypeLiteral<GetMessagesMethod<CassandraId>>(){});
         methods.addBinding().to(new 
TypeLiteral<SetMessagesMethod<CassandraId>>(){});
         bind(SetMessagesUpdateProcessor.class).to(new 
TypeLiteral<SetMessagesUpdateProcessor<CassandraId>>(){});
+        bind(SetMessagesCreationProcessor.class).to(new 
TypeLiteral<SetMessagesCreationProcessor<CassandraId>>(){});
     }
 
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java
 
b/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java
index fcc3101..3acaf8c 100644
--- 
a/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java
+++ 
b/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java
@@ -648,7 +648,6 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    @Ignore("pending SetMessages's send messages feature implementation")
     public void setMessageShouldReturnCreatedMessageWhenSendingMessage() {
         String messageCreationId = "user|inbox|1";
         String requestBody = "[" +
@@ -701,7 +700,6 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    @Ignore("pending SetMessages's send messages feature implementation")
     public void setMessagesShouldCreateMessageInOutboxWhenSendingMessage() 
throws MailboxException {
         // Given
         String messageCreationId = "user|inbox|1";

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/MailboxRoleNotFoundException.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/MailboxRoleNotFoundException.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/MailboxRoleNotFoundException.java
new file mode 100644
index 0000000..88692c8
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/MailboxRoleNotFoundException.java
@@ -0,0 +1,36 @@
+/****************************************************************
+ * 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.exceptions;
+
+import org.apache.james.jmap.model.mailbox.Role;
+
+public class MailboxRoleNotFoundException extends RuntimeException {
+
+    private final Role role;
+
+    public MailboxRoleNotFoundException(Role role) {
+        super(String.format("Could not find any mailbox with role '%s'", 
role.serialize()));
+        this.role = role;
+    }
+
+    public Role getRole() {
+        return role;
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
new file mode 100644
index 0000000..05d0229
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
@@ -0,0 +1,150 @@
+/****************************************************************
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.james.jmap.model.CreationMessage;
+import org.apache.james.jmap.model.Emailer;
+import org.apache.james.mime4j.Charsets;
+import org.apache.james.mime4j.codec.DecodeMonitor;
+import org.apache.james.mime4j.dom.FieldParser;
+import org.apache.james.mime4j.dom.Header;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.mime4j.dom.MessageBuilder;
+import org.apache.james.mime4j.dom.TextBody;
+import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.dom.field.UnstructuredField;
+import org.apache.james.mime4j.field.Fields;
+import org.apache.james.mime4j.field.UnstructuredFieldImpl;
+import org.apache.james.mime4j.io.InputStreams;
+import org.apache.james.mime4j.message.BasicBodyFactory;
+import org.apache.james.mime4j.message.BodyFactory;
+import org.apache.james.mime4j.message.DefaultMessageBuilder;
+import org.apache.james.mime4j.message.DefaultMessageWriter;
+import org.apache.james.mime4j.message.HeaderImpl;
+import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.stream.RawField;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+
+public class MIMEMessageConverter {
+
+    private final MessageBuilder messageBuilder;
+    private final BodyFactory bodyFactory;
+
+    public MIMEMessageConverter() {
+        this.messageBuilder = new DefaultMessageBuilder();
+        this.bodyFactory = new BasicBodyFactory();
+    }
+
+    public byte[] convert(MessageWithId.CreationMessageEntry 
creationMessageEntry) {
+
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        DefaultMessageWriter writer = new DefaultMessageWriter();
+        try {
+            writer.writeMessage(convertToMime(creationMessageEntry), buffer);
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+        return buffer.toByteArray();
+    }
+
+    @VisibleForTesting Message 
convertToMime(MessageWithId.CreationMessageEntry creationMessageEntry) {
+        if (creationMessageEntry == null || creationMessageEntry.getMessage() 
== null) {
+            throw new IllegalArgumentException("creationMessageEntry is either 
null or has null message");
+        }
+
+        Message message = messageBuilder.newMessage();
+        message.setBody(createTextBody(creationMessageEntry.getMessage()));
+        
message.setHeader(buildMimeHeaders(creationMessageEntry.getCreationId(), 
creationMessageEntry.getMessage()));
+        return message;
+    }
+
+    private Header buildMimeHeaders(String creationId, CreationMessage 
newMessage) {
+        Header messageHeaders = new HeaderImpl();
+
+        // add From: and Sender: headers
+        newMessage.getFrom().map(this::convertEmailToMimeHeader)
+                .map(Fields::from)
+                .ifPresent(f -> messageHeaders.addField(f));
+        newMessage.getFrom().map(this::convertEmailToMimeHeader)
+                .map(Fields::sender)
+                .ifPresent(f -> messageHeaders.addField(f));
+
+        // add Reply-To:
+        messageHeaders.addField(Fields.replyTo(newMessage.getReplyTo().stream()
+                .map(rt -> convertEmailToMimeHeader(rt))
+                .collect(Collectors.toList())));
+        // add To: headers
+        messageHeaders.addField(Fields.to(newMessage.getTo().stream()
+                .map(this::convertEmailToMimeHeader)
+                .collect(Collectors.toList())));
+        // add Cc: headers
+        messageHeaders.addField(Fields.cc(newMessage.getCc().stream()
+                .map(this::convertEmailToMimeHeader)
+                .collect(Collectors.toList())));
+        // add Bcc: headers
+        messageHeaders.addField(Fields.bcc(newMessage.getBcc().stream()
+                .map(this::convertEmailToMimeHeader)
+                .collect(Collectors.toList())));
+        // add Subject: header
+        messageHeaders.addField(Fields.subject(newMessage.getSubject()));
+        // set creation Id as MessageId: header
+        messageHeaders.addField(Fields.messageId(creationId));
+
+        // date(String fieldName, Date date, TimeZone zone)
+        // note that date conversion probably lose milliseconds !
+        messageHeaders.addField(Fields.date("Date",
+                Date.from(newMessage.getDate().toInstant()), 
TimeZone.getTimeZone(newMessage.getDate().getZone())
+        ));
+        
newMessage.getInReplyToMessageId().ifPresent(addInReplyToHeader(messageHeaders::addField));
+        return messageHeaders;
+    }
+
+    private Consumer<String> addInReplyToHeader(Consumer<Field> 
headerAppender) {
+        return msgId -> {
+            FieldParser<UnstructuredField> parser = 
UnstructuredFieldImpl.PARSER;
+            RawField rawField = new RawField("In-Reply-To", msgId);
+            headerAppender.accept(parser.parse(rawField, 
DecodeMonitor.SILENT));
+        };
+    }
+
+    private TextBody createTextBody(CreationMessage newMessage) {
+        try {
+            return bodyFactory.textBody(
+                    InputStreams.create(newMessage.getTextBody().orElse(""), 
Charsets.UTF_8),
+                    Charsets.UTF_8.name());
+        } catch (IOException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+
+    private Mailbox convertEmailToMimeHeader(Emailer address) {
+        String[] splitAddress = address.getEmail().split("@", 2);
+        return new Mailbox(address.getName(), null, splitAddress[0], 
splitAddress[1]);
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageWithId.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageWithId.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageWithId.java
new file mode 100644
index 0000000..fbc7105
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageWithId.java
@@ -0,0 +1,48 @@
+/****************************************************************
+ * 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 org.apache.james.jmap.model.CreationMessage;
+
+public class MessageWithId<T> {
+
+    private String creationId;
+    private T message;
+
+    public MessageWithId(String creationId, T message) {
+        this.creationId = creationId;
+        this.message = message;
+    }
+
+    public String getCreationId() {
+        return creationId;
+    }
+
+    public T getMessage() {
+        return message;
+    }
+
+    public static class CreationMessageEntry extends 
MessageWithId<CreationMessage> {
+        public CreationMessageEntry(String creationId, CreationMessage 
message) {
+            super(creationId, message);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
new file mode 100644
index 0000000..3cb11db
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
@@ -0,0 +1,174 @@
+/****************************************************************
+ * 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.util.Date;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.inject.Inject;
+import javax.mail.Flags;
+import javax.mail.internet.SharedInputStream;
+import javax.mail.util.SharedByteArrayInputStream;
+
+import org.apache.james.jmap.exceptions.MailboxRoleNotFoundException;
+import org.apache.james.jmap.model.CreationMessage;
+import org.apache.james.jmap.model.Message;
+import org.apache.james.jmap.model.MessageId;
+import org.apache.james.jmap.model.SetMessagesRequest;
+import org.apache.james.jmap.model.SetMessagesResponse;
+import org.apache.james.jmap.model.mailbox.Role;
+import org.apache.james.mailbox.MailboxManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxMetaData;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.MailboxQuery;
+import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.MailboxMapperFactory;
+import org.apache.james.mailbox.store.mail.MessageMapper;
+import org.apache.james.mailbox.store.mail.model.Mailbox;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
+import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.functions.ThrowingFunction;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+
+public class SetMessagesCreationProcessor<Id extends MailboxId> implements 
SetMessagesProcessor<Id> {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SetMessagesCreationProcessor.class);
+
+    private final MailboxMapperFactory<Id> mailboxMapperFactory;
+    private final MailboxManager mailboxManager;
+    private final MailboxSessionMapperFactory<Id> mailboxSessionMapperFactory;
+    private final MIMEMessageConverter mimeMessageConverter;
+
+    @Inject
+    @VisibleForTesting
+    SetMessagesCreationProcessor(MailboxMapperFactory<Id> mailboxMapperFactory,
+                                 MailboxManager mailboxManager,
+                                 MailboxSessionMapperFactory<Id> 
mailboxSessionMapperFactory, MIMEMessageConverter mimeMessageConverter) {
+        this.mailboxMapperFactory = mailboxMapperFactory;
+        this.mailboxManager = mailboxManager;
+        this.mailboxSessionMapperFactory = mailboxSessionMapperFactory;
+        this.mimeMessageConverter = mimeMessageConverter;
+    }
+
+    @Override
+    public SetMessagesResponse process(SetMessagesRequest request, 
MailboxSession mailboxSession) {
+        Mailbox<Id> outbox;
+        try {
+            outbox = getOutbox(mailboxSession).orElseThrow(() -> new 
MailboxRoleNotFoundException(Role.OUTBOX));
+        } catch (MailboxException | MailboxRoleNotFoundException e) {
+            LOGGER.error("Unable to find a mailbox with role 'outbox'!");
+            throw Throwables.propagate(e);
+        }
+        return request.getCreate().entrySet().stream()
+                .map(e -> new MessageWithId.CreationMessageEntry(e.getKey(), 
e.getValue()))
+                .map(nuMsg -> createMessageInOutbox(nuMsg, mailboxSession, 
outbox, buildMessageIdFunc(mailboxSession, outbox)))
+                .map(msg -> 
SetMessagesResponse.builder().created(ImmutableMap.of(msg.getCreationId(), 
msg.getMessage())).build())
+                .reduce(SetMessagesResponse.builder(), 
SetMessagesResponse.Builder::accumulator, SetMessagesResponse.Builder::combiner)
+                .build();
+    }
+
+    @VisibleForTesting
+    protected MessageWithId<Message> 
createMessageInOutbox(MessageWithId.CreationMessageEntry createdEntry,
+                                                           MailboxSession 
session,
+                                                           Mailbox<Id> outbox, 
Function<Long, MessageId> buildMessageIdFromUid) {
+        try {
+            MessageMapper<Id> messageMapper = 
mailboxSessionMapperFactory.createMessageMapper(session);
+            MailboxMessage<Id> newMailboxMessage = 
buildMailboxMessage(createdEntry, outbox);
+            messageMapper.add(outbox, newMailboxMessage);
+            return new MessageWithId<>(createdEntry.getCreationId(), 
Message.fromMailboxMessage(newMailboxMessage, buildMessageIdFromUid));
+        } catch (MailboxException e) {
+            throw Throwables.propagate(e);
+        } catch (MailboxRoleNotFoundException e) {
+            LOGGER.error("Could not find mailbox '%s' while trying to save 
message.", e.getRole().serialize());
+            throw Throwables.propagate(e);
+        }
+    }
+
+    private Function<Long, MessageId> buildMessageIdFunc(MailboxSession 
session, Mailbox<Id> outbox) {
+        MailboxPath outboxPath = new MailboxPath(session.getPersonalSpace(), 
session.getUser().getUserName(), outbox.getName());
+        return uid -> new MessageId(session.getUser(), outboxPath, uid);
+    }
+
+    private MailboxMessage<Id> 
buildMailboxMessage(MessageWithId.CreationMessageEntry createdEntry, 
Mailbox<Id> outbox) {
+        byte[] messageContent = mimeMessageConverter.convert(createdEntry);
+        SharedInputStream content = new 
SharedByteArrayInputStream(messageContent);
+        long size = messageContent.length;
+        int bodyStartOctet = 0;
+
+        Flags flags = getMessageFlags(createdEntry.getMessage());
+        PropertyBuilder propertyBuilder = buildPropertyBuilder();
+        Id mailboxId = outbox.getMailboxId();
+        Date internalDate = 
Date.from(createdEntry.getMessage().getDate().toInstant());
+
+        return new SimpleMailboxMessage<>(internalDate, size,
+                bodyStartOctet, content, flags, propertyBuilder, mailboxId);
+    }
+
+    @VisibleForTesting
+    protected Optional<Mailbox<Id>> getOutbox(MailboxSession session) throws 
MailboxException {
+        return mailboxManager.search(MailboxQuery.builder(session)
+                .privateUserMailboxes().build(), session).stream()
+            .map(MailboxMetaData::getPath)
+            .filter(this::hasRoleOutbox)
+            .map(loadMailbox(session))
+            .findFirst();
+    }
+
+    private boolean hasRoleOutbox(MailboxPath mailBoxPath) {
+        return Role.from(mailBoxPath.getName())
+                .map(Role.OUTBOX::equals)
+                .orElse(false);
+    }
+
+    private ThrowingFunction<MailboxPath, Mailbox<Id>> 
loadMailbox(MailboxSession session) {
+        return path -> 
mailboxMapperFactory.getMailboxMapper(session).findMailboxByPath(path);
+    }
+
+    private PropertyBuilder buildPropertyBuilder() {
+        return new PropertyBuilder();
+    }
+
+    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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java
index f14cdba..5202026 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java
@@ -56,13 +56,17 @@ public class SetMessagesMethod<Id extends MailboxId> 
implements Method {
     private final MailboxMapperFactory<Id> mailboxMapperFactory;
     private final MailboxSessionMapperFactory<Id> mailboxSessionMapperFactory;
     private final SetMessagesUpdateProcessor<Id> messageUpdater;
+    private final SetMessagesCreationProcessor messageCreator;
 
     @Inject
     @VisibleForTesting SetMessagesMethod(MailboxMapperFactory<Id> 
mailboxMapperFactory,
-                                         MailboxSessionMapperFactory<Id> 
mailboxSessionMapperFactory, SetMessagesUpdateProcessor<Id> messageUpdater) {
+                                         MailboxSessionMapperFactory<Id> 
mailboxSessionMapperFactory,
+                                         SetMessagesUpdateProcessor<Id> 
messageUpdater,
+                                         SetMessagesCreationProcessor 
messageCreator) {
         this.mailboxMapperFactory = mailboxMapperFactory;
         this.mailboxSessionMapperFactory = mailboxSessionMapperFactory;
         this.messageUpdater = messageUpdater;
+        this.messageCreator = messageCreator;
     }
 
     @Override
@@ -95,7 +99,8 @@ public class SetMessagesMethod<Id extends MailboxId> 
implements Method {
     private SetMessagesResponse setMessagesResponse(SetMessagesRequest 
request, MailboxSession mailboxSession) throws MailboxException {
         SetMessagesResponse.Builder responseBuilder = 
SetMessagesResponse.builder();
         processDestroy(request.getDestroy(), mailboxSession, responseBuilder);
-        messageUpdater.processUpdates(request, 
mailboxSession).mergeInto(responseBuilder);
+        messageUpdater.process(request, 
mailboxSession).mergeInto(responseBuilder);
+        messageCreator.process(request, 
mailboxSession).mergeInto(responseBuilder);
         return responseBuilder.build();
     }
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesProcessor.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesProcessor.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesProcessor.java
new file mode 100644
index 0000000..4c7e00d
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesProcessor.java
@@ -0,0 +1,29 @@
+/****************************************************************
+ * 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 org.apache.james.jmap.model.SetMessagesRequest;
+import org.apache.james.jmap.model.SetMessagesResponse;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
+
+public interface SetMessagesProcessor<Id extends MailboxId> {
+    SetMessagesResponse process(SetMessagesRequest request, MailboxSession 
mailboxSession);
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
index f62e390..ed7e219 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessor.java
@@ -52,7 +52,7 @@ import com.google.common.collect.ImmutableSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class SetMessagesUpdateProcessor<Id extends MailboxId> {
+public class SetMessagesUpdateProcessor<Id extends MailboxId> implements 
SetMessagesProcessor<Id> {
 
     private static final int LIMIT_BY_ONE = 1;
     private static final Logger LOGGER = 
LoggerFactory.getLogger(SetMessagesUpdateProcessor.class);
@@ -71,7 +71,7 @@ public class SetMessagesUpdateProcessor<Id extends MailboxId> 
{
         this.mailboxSessionMapperFactory = mailboxSessionMapperFactory;
     }
 
-    public SetMessagesResponse processUpdates(SetMessagesRequest request,  
MailboxSession mailboxSession) {
+    public SetMessagesResponse process(SetMessagesRequest request,  
MailboxSession mailboxSession) {
         SetMessagesResponse.Builder responseBuilder = 
SetMessagesResponse.builder();
         request.buildUpdatePatches(updatePatchConverter).forEach( (id, patch) 
-> {
             if (patch.isValid()) {

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
new file mode 100644
index 0000000..697c9c7
--- /dev/null
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
@@ -0,0 +1,305 @@
+/****************************************************************
+ * 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.time.ZonedDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.james.jmap.methods.GetMessagesMethod;
+import org.apache.james.jmap.methods.JmapResponseWriterImpl;
+
+import com.fasterxml.jackson.annotation.JsonFilter;
+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.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 {
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @JsonPOJOBuilder(withPrefix = "")
+    public static class Builder {
+        private ImmutableList<String> mailboxIds;
+        private String inReplyToMessageId;
+        private boolean isUnread;
+        private boolean isFlagged;
+        private boolean isAnswered;
+        private boolean isDraft;
+        private final ImmutableMap.Builder<String, String> headers;
+        private Emailer from;
+        private final ImmutableList.Builder<Emailer> to;
+        private final ImmutableList.Builder<Emailer> cc;
+        private final ImmutableList.Builder<Emailer> bcc;
+        private final ImmutableList.Builder<Emailer> replyTo;
+        private String subject;
+        private ZonedDateTime date;
+        private String textBody;
+        private String htmlBody;
+        private final ImmutableList.Builder<Attachment> attachments;
+        private final ImmutableMap.Builder<String, SubMessage> 
attachedMessages;
+
+        private Builder() {
+            to = ImmutableList.builder();
+            cc = ImmutableList.builder();
+            bcc = ImmutableList.builder();
+            replyTo = ImmutableList.builder();
+            attachments = ImmutableList.builder();
+            attachedMessages = ImmutableMap.builder();
+            headers = ImmutableMap.builder();
+        }
+
+        public Builder mailboxIds(ImmutableList<String> mailboxIds) {
+            this.mailboxIds = mailboxIds;
+            return this;
+        }
+
+        public Builder inReplyToMessageId(String inReplyToMessageId) {
+            this.inReplyToMessageId = inReplyToMessageId;
+            return this;
+        }
+
+        public Builder isUnread(boolean isUnread) {
+            this.isUnread = isUnread;
+            return this;
+        }
+
+        public Builder isFlagged(boolean isFlagged) {
+            this.isFlagged = isFlagged;
+            return this;
+        }
+
+        public Builder isAnswered(boolean isAnswered) {
+            this.isAnswered = isAnswered;
+            return this;
+        }
+
+        public Builder isDraft(boolean isDraft) {
+            this.isDraft = isDraft;
+            return this;
+        }
+
+        public Builder headers(ImmutableMap<String, String> headers) {
+            this.headers.putAll(headers);
+            return this;
+        }
+
+        public Builder from(Emailer from) {
+            this.from = from;
+            return this;
+        }
+
+        public Builder to(List<Emailer> to) {
+            this.to.addAll(to);
+            return this;
+        }
+
+        public Builder cc(List<Emailer> cc) {
+            this.cc.addAll(cc);
+            return this;
+        }
+
+        public Builder bcc(List<Emailer> bcc) {
+            this.bcc.addAll(bcc);
+            return this;
+        }
+
+        public Builder replyTo(List<Emailer> replyTo) {
+            this.replyTo.addAll(replyTo);
+            return this;
+        }
+
+        public Builder subject(String subject) {
+            this.subject = 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(List<Attachment> attachments) {
+            this.attachments.addAll(attachments);
+            return this;
+        }
+
+        public Builder attachedMessages(Map<String, SubMessage> 
attachedMessages) {
+            this.attachedMessages.putAll(attachedMessages);
+            return this;
+        }
+
+        private static boolean 
areAttachedMessagesKeysInAttachments(ImmutableList<Attachment> attachments, 
ImmutableMap<String, SubMessage> attachedMessages) {
+            return attachments.stream()
+                    .map(Attachment::getBlobId)
+                    .allMatch(attachedMessages::containsKey);
+        }
+
+        public CreationMessage build() {
+            Preconditions.checkState(mailboxIds != null, "'mailboxIds' is 
mandatory");
+            Preconditions.checkState(headers != null, "'headers' is 
mandatory");
+            Preconditions.checkState(!Strings.isNullOrEmpty(subject), 
"'subject' is mandatory");
+            ImmutableList<Attachment> attachments = this.attachments.build();
+            ImmutableMap<String, SubMessage> attachedMessages = 
this.attachedMessages.build();
+            
Preconditions.checkState(areAttachedMessagesKeysInAttachments(attachments, 
attachedMessages), "'attachedMessages' keys must be in 'attachments'");
+
+            if (date == null) {
+                date = ZonedDateTime.now();
+            }
+
+            return new CreationMessage(mailboxIds, 
Optional.ofNullable(inReplyToMessageId), isUnread, isFlagged, isAnswered, 
isDraft, headers.build(), Optional.ofNullable(from),
+                    to.build(), cc.build(), bcc.build(), replyTo.build(), 
subject, date, Optional.ofNullable(textBody), Optional.ofNullable(htmlBody), 
attachments, attachedMessages);
+        }
+    }
+
+    private final ImmutableList<String> mailboxIds;
+    private final Optional<String> inReplyToMessageId;
+    private final boolean isUnread;
+    private final boolean isFlagged;
+    private final boolean isAnswered;
+    private final boolean isDraft;
+    private final ImmutableMap<String, String> headers;
+    private final Optional<Emailer> from;
+    private final ImmutableList<Emailer> to;
+    private final ImmutableList<Emailer> cc;
+    private final ImmutableList<Emailer> bcc;
+    private final ImmutableList<Emailer> 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<String, SubMessage> attachedMessages;
+
+    @VisibleForTesting
+    CreationMessage(ImmutableList<String> mailboxIds, Optional<String> 
inReplyToMessageId, boolean isUnread, boolean isFlagged, boolean isAnswered, 
boolean isDraft, ImmutableMap<String, String> headers, Optional<Emailer> from,
+                    ImmutableList<Emailer> to, ImmutableList<Emailer> cc, 
ImmutableList<Emailer> bcc, ImmutableList<Emailer> replyTo, String subject, 
ZonedDateTime date, Optional<String> textBody, Optional<String> htmlBody, 
ImmutableList<Attachment> attachments,
+                    ImmutableMap<String, SubMessage> attachedMessages) {
+        this.mailboxIds = mailboxIds;
+        this.inReplyToMessageId = inReplyToMessageId;
+        this.isUnread = isUnread;
+        this.isFlagged = isFlagged;
+        this.isAnswered = isAnswered;
+        this.isDraft = isDraft;
+        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;
+    }
+
+    public ImmutableList<String> getMailboxIds() {
+        return mailboxIds;
+    }
+
+    public Optional<String> getInReplyToMessageId() {
+        return inReplyToMessageId;
+    }
+
+    public boolean isIsUnread() {
+        return isUnread;
+    }
+
+    public boolean isIsFlagged() {
+        return isFlagged;
+    }
+
+    public boolean isIsAnswered() {
+        return isAnswered;
+    }
+
+    public boolean isIsDraft() {
+        return isDraft;
+    }
+
+    public ImmutableMap<String, String> getHeaders() {
+        return headers;
+    }
+
+    public Optional<Emailer> getFrom() {
+        return from;
+    }
+
+    public ImmutableList<Emailer> getTo() {
+        return to;
+    }
+
+    public ImmutableList<Emailer> getCc() {
+        return cc;
+    }
+
+    public ImmutableList<Emailer> getBcc() {
+        return bcc;
+    }
+
+    public ImmutableList<Emailer> 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<String, SubMessage> getAttachedMessages() {
+        return attachedMessages;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
index aadb886..8e25ac5 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
@@ -48,7 +48,7 @@ public class SetMessagesRequest implements JmapRequest {
 
         private String accountId;
         private String ifInState;
-        private ImmutableMap.Builder<MessageId, Message> create;
+        private ImmutableMap.Builder<String, CreationMessage> create;
         private ImmutableMap.Builder<MessageId, 
Function<UpdateMessagePatchConverter, UpdateMessagePatch>> updatesProvider;
 
         private ImmutableList.Builder<MessageId> destroy;
@@ -73,8 +73,8 @@ public class SetMessagesRequest implements JmapRequest {
             return this;
         }
 
-        public Builder create(Map<MessageId, Message> creates) {
-            this.create.putAll(creates);
+        public Builder create(Map<String, CreationMessage> creations) {
+            this.create.putAll(creations);
             return this;
         }
 
@@ -95,11 +95,11 @@ public class SetMessagesRequest implements JmapRequest {
 
     private final Optional<String> accountId;
     private final Optional<String> ifInState;
-    private final Map<MessageId, Message> create;
+    private final Map<String, CreationMessage> create;
     private final Map<MessageId, Function<UpdateMessagePatchConverter, 
UpdateMessagePatch>> update;
     private final List<MessageId> destroy;
 
-    @VisibleForTesting SetMessagesRequest(Optional<String> accountId, 
Optional<String> ifInState, Map<MessageId, Message> create, Map<MessageId, 
Function<UpdateMessagePatchConverter, UpdateMessagePatch>>  update, 
List<MessageId> destroy) {
+    @VisibleForTesting SetMessagesRequest(Optional<String> accountId, 
Optional<String> ifInState, Map<String, CreationMessage> create, Map<MessageId, 
Function<UpdateMessagePatchConverter, UpdateMessagePatch>>  update, 
List<MessageId> destroy) {
         this.accountId = accountId;
         this.ifInState = ifInState;
         this.create = create;
@@ -115,7 +115,7 @@ public class SetMessagesRequest implements JmapRequest {
         return ifInState;
     }
 
-    public Map<MessageId, Message> getCreate() {
+    public Map<String, CreationMessage> getCreate() {
         return create;
     }
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
index 304f8c3..c096c8a 100644
--- 
a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
+++ 
b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
@@ -20,9 +20,11 @@ package org.apache.james.jmap.model;
 
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 
 import com.google.common.base.Strings;
 import org.apache.commons.lang.NotImplementedException;
+import org.apache.james.jmap.methods.MessageWithId;
 import org.apache.james.jmap.methods.Method;
 
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@@ -42,10 +44,18 @@ public class SetMessagesResponse implements Method.Response 
{
     @JsonPOJOBuilder(withPrefix = "")
     public static class Builder {
 
+        public static Builder accumulator(Builder accumulator, 
SetMessagesResponse response) {
+            return response.mergeInto(accumulator);
+        }
+
+        public static Builder combiner(Builder firstBuilder, Builder 
secondBuilder) {
+            return secondBuilder.build().mergeInto(firstBuilder);
+        }
+
         private String accountId;
         private String oldState;
         private String newState;
-        private ImmutableList.Builder<Message> created;
+        private ImmutableMap.Builder<String, Message> created;
         private ImmutableList.Builder<MessageId> updated;
         private ImmutableList.Builder<MessageId> destroyed;
         private ImmutableMap.Builder<MessageId, SetError> notCreated;
@@ -53,7 +63,7 @@ public class SetMessagesResponse implements Method.Response {
         private ImmutableMap.Builder<MessageId, SetError> notDestroyed;
 
         private Builder() {
-            created = ImmutableList.builder();
+            created = ImmutableMap.builder();
             updated = ImmutableList.builder();
             destroyed = ImmutableList.builder();
             notCreated = ImmutableMap.builder();
@@ -73,8 +83,8 @@ public class SetMessagesResponse implements Method.Response {
             throw new NotImplementedException();
         }
 
-        public Builder created(List<Message> created) {
-            this.created.addAll(created);
+        public Builder created(Map<String, Message> created) {
+            this.created.putAll(created);
             return this;
         }
 
@@ -122,14 +132,14 @@ public class SetMessagesResponse implements 
Method.Response {
     private final String accountId;
     private final String oldState;
     private final String newState;
-    private final List<Message> created;
+    private final Map<String, Message> created;
     private final List<MessageId> updated;
     private final List<MessageId> destroyed;
     private final Map<MessageId, SetError> notCreated;
     private final Map<MessageId, SetError> notUpdated;
     private final Map<MessageId, SetError> notDestroyed;
 
-    @VisibleForTesting SetMessagesResponse(String accountId, String oldState, 
String newState, List<Message> created, List<MessageId> updated, 
List<MessageId> destroyed, 
+    @VisibleForTesting SetMessagesResponse(String accountId, String oldState, 
String newState, Map<String, Message> created, List<MessageId> updated, 
List<MessageId> destroyed,
             Map<MessageId, SetError> notCreated, Map<MessageId, SetError> 
notUpdated, Map<MessageId, SetError> notDestroyed) {
         this.accountId = accountId;
         this.oldState = oldState;
@@ -158,7 +168,7 @@ public class SetMessagesResponse implements Method.Response 
{
     }
 
     @JsonSerialize
-    public List<Message> getCreated() {
+    public Map<String, Message> getCreated() {
         return created;
     }
 
@@ -187,7 +197,7 @@ public class SetMessagesResponse implements Method.Response 
{
         return notDestroyed;
     }
 
-    public void mergeInto(SetMessagesResponse.Builder responseBuilder) {
+    public SetMessagesResponse.Builder mergeInto(SetMessagesResponse.Builder 
responseBuilder) {
         responseBuilder.created(getCreated());
         responseBuilder.updated(getUpdated());
         responseBuilder.destroyed(getDestroyed());
@@ -203,5 +213,6 @@ public class SetMessagesResponse implements Method.Response 
{
         if(! Strings.isNullOrEmpty(getNewState())) {
             responseBuilder.accountId(getAccountId());
         }
+        return responseBuilder;
     }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
new file mode 100644
index 0000000..0b73ee2
--- /dev/null
+++ 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java
@@ -0,0 +1,131 @@
+/****************************************************************
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.apache.james.jmap.model.CreationMessage;
+import org.apache.james.jmap.model.Emailer;
+import org.apache.james.jmap.model.Message;
+import org.apache.james.jmap.model.MessageId;
+import org.apache.james.jmap.model.SetMessagesRequest;
+import org.apache.james.jmap.model.SetMessagesResponse;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.MessageMapper;
+import org.apache.james.mailbox.store.mail.model.Mailbox;
+import org.apache.james.mailbox.store.mail.model.MailboxId;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class SetMessagesCreationProcessorTest {
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void processShouldReturnEmptyCreatedWhenRequestHasEmptyCreate() {
+        SetMessagesCreationProcessor<MailboxId> sut = new 
SetMessagesCreationProcessor<MailboxId>(null, null, null, null) {
+            @Override
+            protected Optional<Mailbox<MailboxId>> getOutbox(MailboxSession 
session) throws MailboxException {
+                Mailbox<MailboxId> fakeOutbox = mock(Mailbox.class);
+                when(fakeOutbox.getName()).thenReturn("outbox");
+                return Optional.of(fakeOutbox);
+            }
+        };
+        SetMessagesRequest requestWithEmptyCreate = 
SetMessagesRequest.builder().build();
+
+        SetMessagesResponse result = sut.process(requestWithEmptyCreate, 
buildStubbedSession());
+
+        assertThat(result.getCreated()).isEmpty();
+        assertThat(result.getNotCreated()).isEmpty();
+    }
+
+    private MailboxSession buildStubbedSession() {
+        MailboxSession.User stubUser = mock(MailboxSession.User.class);
+        when(stubUser.getUserName()).thenReturn("user");
+        MailboxSession stubSession = mock(MailboxSession.class);
+        when(stubSession.getPathDelimiter()).thenReturn('.');
+        when(stubSession.getUser()).thenReturn(stubUser);
+        when(stubSession.getPersonalSpace()).thenReturn("#private");
+        return stubSession;
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void 
processShouldReturnNonEmptyCreatedWhenRequestHasNonEmptyCreate() throws 
MailboxException {
+
+        MessageMapper<MailboxId> stubMapper = mock(MessageMapper.class);
+        MailboxSessionMapperFactory<MailboxId> mockSessionMapperFactory = 
mock(MailboxSessionMapperFactory.class);
+        
when(mockSessionMapperFactory.createMessageMapper(any(MailboxSession.class)))
+                .thenReturn(stubMapper);
+
+        SetMessagesCreationProcessor<MailboxId> sut = new 
SetMessagesCreationProcessor<MailboxId>(null, null, mockSessionMapperFactory, 
null) {
+            @Override
+            protected MessageWithId<Message> 
createMessageInOutbox(MessageWithId.CreationMessageEntry createdEntry, 
MailboxSession session, Mailbox<MailboxId> outbox, Function<Long, MessageId> 
buildMessageIdFromUid) {
+                return new MessageWithId<>(createdEntry.creationId, 
getFakeMessage());
+            }
+            @Override
+            protected Optional<Mailbox<MailboxId>> getOutbox(MailboxSession 
session) throws MailboxException {
+                Mailbox<MailboxId> fakeOutbox = mock(Mailbox.class);
+                when(fakeOutbox.getName()).thenReturn("outbox");
+                return Optional.of(fakeOutbox);
+            }
+        };
+
+        SetMessagesRequest creationRequest = SetMessagesRequest.builder()
+                .create(ImmutableMap.of("anything-really", 
CreationMessage.builder()
+                    
.from(Emailer.builder().name("alice").email("[email protected]").build())
+                    
.to(ImmutableList.of(Emailer.builder().name("bob").email("[email protected]").build()))
+                    .subject("Hey! ")
+                    .mailboxIds(ImmutableList.of("mailboxId"))
+                    .build()
+                ))
+                .build()
+                ;
+
+        SetMessagesResponse result = sut.process(creationRequest, 
buildStubbedSession());
+
+        assertThat(result.getCreated()).isNotEmpty();
+        assertThat(result.getNotCreated()).isEmpty();
+    }
+
+    private Message getFakeMessage() {
+        return Message.builder()
+                .id(org.apache.james.jmap.model.MessageId.of("user|outbox|1"))
+                .blobId("anything")
+                .threadId("anything")
+                .mailboxIds(ImmutableList.of("mailboxId"))
+                .headers(ImmutableMap.of())
+                .subject("anything")
+                .size(0)
+                .date(ZonedDateTime.now())
+                .preview("anything")
+                .build();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/99ac325d/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
----------------------------------------------------------------------
diff --git 
a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
index e2bc7c7..9877651 100644
--- 
a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
+++ 
b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
@@ -55,7 +55,7 @@ public class SetMessagesResponseTest {
     @Test
     public void builderShouldWork() {
         ZonedDateTime currentDate = ZonedDateTime.now();
-        ImmutableList<Message> created = ImmutableList.of(
+        ImmutableMap<String, Message> created = 
ImmutableMap.of("user|created|1",
             Message.builder()
                 .id(MessageId.of("user|created|1"))
                 .blobId("blobId")
@@ -105,8 +105,8 @@ public class SetMessagesResponseTest {
         
assertThat(emptyBuilder.build()).isEqualToComparingFieldByField(testee);
     }
 
-    private ImmutableList<Message> buildMessage(String messageId) {
-        return ImmutableList.of(Message.builder()
+    private ImmutableMap<String, Message> buildMessage(String messageId) {
+        return ImmutableMap.of(messageId, Message.builder()
                 .id(MessageId.of(messageId))
                 .blobId("blobId")
                 .threadId("threadId")
@@ -152,10 +152,7 @@ public class SetMessagesResponseTest {
         SetMessagesResponse mergedResponse = nonEmptyBuilder.build();
 
         // Then
-        List<String> createdMessageIds = mergedResponse.getCreated().stream()
-                .map(m -> m.getId().serialize())
-                .collect(Collectors.toList());
-        
assertThat(createdMessageIds).containsExactly(buildersCreatedMessageId, 
createdMessageId);
+        
assertThat(mergedResponse.getCreated().keySet()).containsExactly(buildersCreatedMessageId,
 createdMessageId);
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to