You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by ad...@apache.org on 2017/11/16 14:19:57 UTC

[10/18] james-project git commit: JAMES-2220 Implement sending draft with move to Outbox

JAMES-2220 Implement sending draft with move to Outbox


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

Branch: refs/heads/master
Commit: 29d233cc497e6e7a6f62f38285e751940fae5e48
Parents: 85ad874
Author: Matthieu Baechler <ma...@apache.org>
Authored: Tue Nov 14 21:40:30 2017 +0100
Committer: Antoine Duprat <ad...@linagora.com>
Committed: Thu Nov 16 12:30:31 2017 +0100

----------------------------------------------------------------------
 .../org/apache/james/server/core/MailImpl.java  |  29 +++
 .../integration/SetMessagesMethodTest.java      | 214 ++++++++++++++++++-
 .../exceptions/InvalidOutboxMoveException.java  |  23 ++
 .../james/jmap/methods/MessageSender.java       |  19 +-
 .../methods/SetMessagesUpdateProcessor.java     | 102 +++++++--
 .../org/apache/james/jmap/send/MailFactory.java |   2 +
 .../methods/SetMessagesUpdateProcessorTest.java |  16 +-
 7 files changed, 386 insertions(+), 19 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java
----------------------------------------------------------------------
diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java b/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java
index c5c7f9b..af9020a 100644
--- a/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java
+++ b/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java
@@ -28,6 +28,7 @@ import java.io.ObjectOutputStream;
 import java.io.OptionalDataException;
 import java.io.OutputStream;
 import java.io.Serializable;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
@@ -35,7 +36,10 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.UUID;
 
+import javax.mail.Address;
 import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeMessage;
 import javax.mail.internet.ParseException;
 
@@ -49,6 +53,8 @@ import org.apache.mailet.PerRecipientHeaders.Header;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.fge.lambdas.Throwing;
+import com.github.steveash.guavate.Guavate;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -84,6 +90,29 @@ public class MailImpl implements Disposable, Mail {
         return new MailImpl(mail, deriveNewName(mail.getName()));
     }
 
+    public static MailImpl fromMimeMessage(String name, MimeMessage mimeMessage) throws MessagingException {
+        MailAddress sender = getSender(mimeMessage);
+        ImmutableList<MailAddress> recipients = getRecipients(mimeMessage);
+        return new MailImpl(name, sender, recipients, mimeMessage);
+    }
+
+    private static ImmutableList<MailAddress> getRecipients(MimeMessage mimeMessage) throws MessagingException {
+        return Arrays.stream(mimeMessage.getAllRecipients())
+            .map(Throwing.function(MailImpl::castToMailAddress).sneakyThrow())
+            .collect(Guavate.toImmutableList());
+    }
+
+    private static MailAddress getSender(MimeMessage mimeMessage) throws MessagingException {
+        Address[] sender = mimeMessage.getFrom();
+        Preconditions.checkArgument(sender.length == 1);
+        return castToMailAddress(sender[0]);
+    }
+
+    private static MailAddress castToMailAddress(Address address) throws AddressException {
+        Preconditions.checkArgument(address instanceof InternetAddress);
+        return new MailAddress((InternetAddress) address);
+    }
+
     /**
      * Create a unique new primary key name for the given MailObject.
      * Detect if this has been called more than 8 times recursively

http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
index c29302b..924c4a2 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
@@ -198,6 +198,11 @@ public abstract class SetMessagesMethodTest {
         return getMailboxId(accessToken, Role.DRAFTS);
     }
 
+    private String getInboxId(AccessToken accessToken) {
+        return getMailboxId(accessToken, Role.INBOX);
+    }
+
+
     private String getMailboxId(AccessToken accessToken, Role role) {
         return getAllMailboxesIds(accessToken).stream()
             .filter(x -> x.get("role").equalsIgnoreCase(role.serialize()))
@@ -564,7 +569,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    @Ignore("gitlab-446 should allowed in drafts mailbox, rejected outside")
+    @Ignore("JAMES-2220 should allowed in drafts mailbox, rejected outside")
     public void setMessagesShouldReturnAnErrorWhenKeywordsWithAddingDraftArePassed() throws MailboxException {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
 
@@ -700,7 +705,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    @Ignore("gitlab-446 should allowed outside drafts mailbox, rejected inside")
+    @Ignore("JAMES-2220 should allowed outside drafts mailbox, rejected inside")
     public void setMessagesShouldReturnAnErrorWhenKeywordsWithRemoveDraftArePassed() throws MailboxException {
         mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox");
 
@@ -1445,7 +1450,7 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
-    public void setMessageShouldAllowDraftCreationWhenUsingIsDraftProperty() {
+    public void setMessagesShouldAllowDraftCreationWhenUsingIsDraftProperty() {
         String messageCreationId = "creationId1337";
         String fromAddress = USERNAME;
         String requestBody = "[" +
@@ -1894,6 +1899,209 @@ public abstract class SetMessagesMethodTest {
     }
 
     @Test
+    public void setMessagesShouldSendMessageByMovingDraftToOutbox() {
+        String draftCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String createDraft = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"create\": { \"" + draftCreationId  + "\" : {" +
+            "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+            "        \"to\": [{ \"name\": \"BOB\", \"email\": \"" + BOB + "\"}]," +
+            "        \"subject\": \"subject\"," +
+            "        \"keywords\": {\"$Draft\": true}," +
+            "        \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        String draftId =
+            with()
+                .header("Authorization", accessToken.serialize())
+                .body(createDraft)
+                .post("/jmap")
+            .then()
+                .extract()
+                .path(ARGUMENTS + ".created[\"" + draftCreationId + "\"].id");
+
+        String moveDraftToOutBox = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"update\": { \"" + draftId + "\" : {" +
+            "        \"keywords\": {}," +
+            "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body(moveDraftToOutBox)
+            .post("/jmap");
+
+        calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> isAnyMessageFoundInRecipientsMailboxes(bobAccessToken));
+    }
+
+    @Test
+    @Ignore("WIP")
+    public void setMessagesShouldRejectDraftCopyToOutbox() {
+        String draftCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String createDraft = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"create\": { \"" + draftCreationId  + "\" : {" +
+            "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+            "        \"to\": [{ \"name\": \"BOB\", \"email\": \"" + BOB + "\"}]," +
+            "        \"subject\": \"subject\"," +
+            "        \"keywords\": {\"$Draft\": true}," +
+            "        \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        String draftId =
+            with()
+                .header("Authorization", accessToken.serialize())
+                .body(createDraft)
+                .post("/jmap")
+            .then()
+                .extract()
+                .path(ARGUMENTS + ".created[\"" + draftCreationId + "\"].id");
+
+        String copyDraftToOutBox = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"update\": { \"" + draftId + "\" : {" +
+            "        \"keywords\": {}," +
+            "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\",\"" + getDraftId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+        
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(copyDraftToOutBox)
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notUpdated", hasKey(draftId))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].type", equalTo("invalidArguments"))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].description", endsWith("One can not have a message in mailboxes that don't have all the `draft` role"))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].properties", hasSize(1))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].properties", contains("mailboxIds"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldRejectMovingMessageToOutboxWhenNotInDraft() throws MailboxException {
+        ComposedMessageId message = mailboxProbe.appendMessage(USERNAME, MailboxPath.forUser(USERNAME, MailboxConstants.INBOX),
+            new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
+        await();
+
+        String messageId = message.getMessageId().serialize();
+        String moveMessageToOutBox = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"update\": { \"" + messageId + "\" : {" +
+            "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(moveMessageToOutBox)
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notUpdated", hasKey(messageId))
+            .body(ARGUMENTS + ".notUpdated[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(ARGUMENTS + ".notUpdated[\""+messageId+"\"].description", endsWith("only drafts can be moved to Outbox"))
+            .body(ARGUMENTS + ".notUpdated[\""+messageId+"\"].properties", hasSize(1))
+            .body(ARGUMENTS + ".notUpdated[\""+messageId+"\"].properties", contains("mailboxIds"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldRejectMovingMessageToOutboxWhenDraftKeyworkSet() throws MailboxException {
+        String draftCreationId = "creationId1337";
+        String fromAddress = USERNAME;
+        String createDraft = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"create\": { \"" + draftCreationId  + "\" : {" +
+            "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+            "        \"to\": [{ \"name\": \"BOB\", \"email\": \"" + BOB + "\"}]," +
+            "        \"subject\": \"subject\"," +
+            "        \"keywords\": {\"$Draft\": true}," +
+            "        \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        String draftId =
+            with()
+                .header("Authorization", accessToken.serialize())
+                .body(createDraft)
+                .post("/jmap")
+                .then()
+                .extract()
+                .path(ARGUMENTS + ".created[\"" + draftCreationId + "\"].id");
+
+        String moveDraftToOutBox = "[" +
+            "  [" +
+            "    \"setMessages\","+
+            "    {" +
+            "      \"update\": { \"" + draftId + "\" : {" +
+            "        \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(moveDraftToOutBox)
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notUpdated", hasKey(draftId))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].type", equalTo("invalidProperties"))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].description", endsWith("message with $Draft keyword can't be moved outside outbox"))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].properties", hasSize(1))
+            .body(ARGUMENTS + ".notUpdated[\""+draftId+"\"].properties", contains("keywords"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
+    }
+
+
+    @Test
     public void setMessagesShouldSupportArbitraryMessageId() {
         String messageCreationId = "1717fcd1-603e-44a5-b2a6-1234dbcd5723";
         String fromAddress = USERNAME;

http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidOutboxMoveException.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidOutboxMoveException.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidOutboxMoveException.java
new file mode 100644
index 0000000..a70a750
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidOutboxMoveException.java
@@ -0,0 +1,23 @@
+/****************************************************************
+ * 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;
+
+public class InvalidOutboxMoveException extends RuntimeException {
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageSender.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageSender.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageSender.java
index 3ce4086..d6835c9 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageSender.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageSender.java
@@ -22,6 +22,7 @@ package org.apache.james.jmap.methods;
 import javax.inject.Inject;
 import javax.mail.MessagingException;
 
+import org.apache.james.core.MailAddress;
 import org.apache.james.jmap.model.Envelope;
 import org.apache.james.jmap.model.MessageFactory;
 import org.apache.james.jmap.send.MailFactory;
@@ -30,6 +31,7 @@ import org.apache.james.jmap.send.MailSpool;
 import org.apache.james.lifecycle.api.LifecycleUtil;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MessageId;
 import org.apache.mailet.Mail;
 
 public class MessageSender {
@@ -55,9 +57,22 @@ public class MessageSender {
         }
     }
 
+    public void sendMessage(MessageId messageId,
+                            Mail mail,
+                            MailboxSession session) throws MailboxException, MessagingException {
+        assertUserIsSender(session, mail.getSender());
+        MailMetadata metadata = new MailMetadata(messageId, session.getUser().getUserName());
+        mailSpool.send(mail, metadata);
+    }
+
     private void assertUserIsInSenders(Envelope envelope, MailboxSession session) throws MailboxSendingNotAllowedException {
-        String allowedSender = session.getUser().getUserName();
-        if (!session.getUser().isSameUser(envelope.getFrom().asString())) {
+        MailAddress sender = envelope.getFrom();
+        assertUserIsSender(session, sender);
+    }
+
+    private void assertUserIsSender(MailboxSession session, MailAddress sender) throws MailboxSendingNotAllowedException {
+        if (!session.getUser().isSameUser(sender.asString())) {
+            String allowedSender = session.getUser().getUserName();
             throw new MailboxSendingNotAllowedException(allowedSender);
         }
     }

http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/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 c54deec..e3865c0 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
@@ -21,16 +21,23 @@ package org.apache.james.jmap.methods;
 
 import static org.apache.james.jmap.methods.Method.JMAP_PREFIX;
 
+import java.io.IOException;
+import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
+import java.util.Properties;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
 import javax.mail.Flags;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
 
 import org.apache.james.jmap.exceptions.DraftMessageMailboxUpdateException;
+import org.apache.james.jmap.exceptions.InvalidOutboxMoveException;
 import org.apache.james.jmap.model.MessageProperties;
 import org.apache.james.jmap.model.SetError;
 import org.apache.james.jmap.model.SetMessagesRequest;
@@ -42,6 +49,7 @@ import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageIdManager;
 import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.model.FetchGroupImpl;
 import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MailboxId.Factory;
@@ -49,6 +57,8 @@ import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.MessageResult;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.metrics.api.TimeMetric;
+import org.apache.james.server.core.MailImpl;
+import org.apache.james.util.OptionalUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -67,17 +77,22 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
     private final SystemMailboxesProvider systemMailboxesProvider;
     private final Factory mailboxIdFactory;
     private final MetricFactory metricFactory;
+    private final MessageSender messageSender;
 
     @Inject
     @VisibleForTesting SetMessagesUpdateProcessor(
-        UpdateMessagePatchConverter updatePatchConverter,
-        MessageIdManager messageIdManager,
-        SystemMailboxesProvider systemMailboxesProvider, Factory mailboxIdFactory, MetricFactory metricFactory) {
+            UpdateMessagePatchConverter updatePatchConverter,
+            MessageIdManager messageIdManager,
+            SystemMailboxesProvider systemMailboxesProvider,
+            Factory mailboxIdFactory,
+            MessageSender messageSender,
+            MetricFactory metricFactory) {
         this.updatePatchConverter = updatePatchConverter;
         this.messageIdManager = messageIdManager;
         this.systemMailboxesProvider = systemMailboxesProvider;
         this.mailboxIdFactory = mailboxIdFactory;
         this.metricFactory = metricFactory;
+        this.messageSender = messageSender;
     }
 
     @Override
@@ -116,10 +131,18 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
                 } else {
                     builder.updated(ImmutableList.of(messageId));
                 }
+                sendMessageWhenOutboxInTargetMailboxIds(messageId, updateMessagePatch, mailboxSession, builder);
             }
         } catch (DraftMessageMailboxUpdateException e) {
             handleDraftMessageMailboxUpdateException(messageId, builder, e);
-        } catch (MailboxException e) {
+        } catch (InvalidOutboxMoveException e) {
+            ValidationResult invalidPropertyMailboxIds = ValidationResult.builder()
+                .property(MessageProperties.MessageProperty.mailboxIds.asFieldName())
+                .message("only drafts can be moved to Outbox")
+                .build();
+
+            handleInvalidRequest(builder, messageId, ImmutableList.of(invalidPropertyMailboxIds));
+        } catch (MailboxException|IOException|MessagingException e) {
             handleMessageUpdateException(messageId, builder, e);
         } catch (IllegalArgumentException e) {
             ValidationResult invalidPropertyKeywords = ValidationResult.builder()
@@ -129,24 +152,54 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
 
             handleInvalidRequest(builder, messageId, ImmutableList.of(invalidPropertyKeywords));
         }
+    }
 
+    private void sendMessageWhenOutboxInTargetMailboxIds(MessageId messageId, UpdateMessagePatch updateMessagePatch, MailboxSession mailboxSession, SetMessagesResponse.Builder builder) throws MailboxException, MessagingException, IOException {
+        if (isTargetingOutbox(mailboxSession, listTargetMailboxIds(updateMessagePatch))) {
+            Optional<MessageResult> messagesToSend =
+                messageIdManager.getMessages(
+                    ImmutableList.of(messageId), FetchGroupImpl.FULL_CONTENT, mailboxSession)
+                    .stream()
+                    .findFirst();
+            if (messagesToSend.isPresent()) {
+                MailImpl mail = buildMailFromMessage(messagesToSend.get());
+                messageSender.sendMessage(messageId, mail, mailboxSession);
+            } else {
+                addMessageIdNotFoundToResponse(messageId, builder);
+            }
+        }
     }
 
     private void assertValidUpdate(List<MessageResult> messagesToBeUpdated, UpdateMessagePatch updateMessagePatch, MailboxSession session) throws MailboxException {
         List<MailboxId> draftMailboxes = mailboxIdFor(Role.DRAFTS, session);
+        List<MailboxId> outboxMailboxes = mailboxIdFor(Role.OUTBOX, session);
+
+        ImmutableList<MailboxId> previousMailboxes = messagesToBeUpdated.stream()
+            .map(MessageResult::getMailboxId)
+            .collect(Guavate.toImmutableList());
         List<Flags> futureFlags = patchFlags(messagesToBeUpdated, updateMessagePatch);
-        List<MailboxId> targetMailboxes = getTargetedMailboxes(messagesToBeUpdated, updateMessagePatch);
+        List<MailboxId> targetMailboxes = getTargetedMailboxes(previousMailboxes, updateMessagePatch);
 
+        boolean originIsDraft = previousMailboxes.stream().allMatch(draftMailboxes::contains);
+        boolean targetIsOutbox = targetMailboxes.stream().anyMatch(outboxMailboxes::contains);
         boolean targetHasDraft = targetMailboxes.stream().anyMatch(draftMailboxes::contains);
         boolean targetHasNonDraft = targetMailboxes.stream().anyMatch(id -> !draftMailboxes.contains(id));
 
-        assertValidUpdate(futureFlags, targetHasDraft, targetHasNonDraft);
+        assertValidUpdate(futureFlags, targetHasDraft, targetHasNonDraft, targetIsOutbox, originIsDraft);
     }
 
-    private void assertValidUpdate(List<Flags> futureFlags, boolean targetHasDraft, boolean targetHasNonDraft) throws DraftMessageMailboxUpdateException {
+    private void assertValidUpdate(List<Flags> futureFlags, boolean targetHasDraft, boolean targetHasNonDraft,
+                                   boolean targetIsOutbox, boolean originIsDraft) throws DraftMessageMailboxUpdateException {
         assertMessageIsNotInDraftAndNonDraftMailboxes(targetHasDraft, targetHasNonDraft);
         assertNoNonDraftMessageInsideDraftMailbox(futureFlags, targetHasDraft);
         assertNoDraftMessageOutOfDraftMailbox(futureFlags, targetHasNonDraft);
+        assertOutboxMoveOnlyFromDraft(targetIsOutbox, originIsDraft);
+    }
+
+    private void assertOutboxMoveOnlyFromDraft(boolean targetIsOutbox, boolean originIsDraft) {
+        if (targetIsOutbox && !originIsDraft) {
+            throw new InvalidOutboxMoveException();
+        }
     }
 
     private void assertMessageIsNotInDraftAndNonDraftMailboxes(boolean targetHasDraft, boolean targetHasNonDraft) throws DraftMessageMailboxUpdateException {
@@ -173,10 +226,7 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
         }
     }
 
-    private List<MailboxId> getTargetedMailboxes(List<MessageResult> messagesToBeUpdated, UpdateMessagePatch updateMessagePatch) {
-        ImmutableList<MailboxId> previousMailboxes = messagesToBeUpdated.stream()
-            .map(MessageResult::getMailboxId)
-            .collect(Guavate.toImmutableList());
+    private List<MailboxId> getTargetedMailboxes(ImmutableList<MailboxId> previousMailboxes, UpdateMessagePatch updateMessagePatch) {
         return updateMessagePatch.getMailboxIds()
             .map(ids -> ids.stream().map(mailboxIdFactory::fromString).collect(Guavate.toImmutableList()))
             .orElse(previousMailboxes);
@@ -195,6 +245,34 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
             .collect(Guavate.toImmutableList());
     }
 
+    private MailImpl buildMailFromMessage(MessageResult message) throws MessagingException, IOException, MailboxException {
+        return MailImpl.fromMimeMessage(message.getMessageId().serialize(),
+            new MimeMessage(
+                Session.getDefaultInstance(new Properties()),
+                message.getFullContent().getInputStream()));
+    }
+
+    private Set<MailboxId> listTargetMailboxIds(UpdateMessagePatch updateMessagePatch) {
+        return OptionalUtils.toStream(updateMessagePatch.getMailboxIds())
+            .flatMap(Collection::stream)
+            .map(mailboxIdFactory::fromString)
+            .collect(Guavate.toImmutableSet());
+    }
+
+    private boolean isTargetingOutbox(MailboxSession mailboxSession, Set<MailboxId> targetMailboxIds) throws MailboxException {
+        Set<MailboxId> outboxes = listMailboxIdsForRole(mailboxSession, Role.OUTBOX);
+        if (outboxes.isEmpty()) {
+            throw new MailboxNotFoundException("At least one outbox should be accessible");
+        }
+        return targetMailboxIds.stream().anyMatch(outboxes::contains);
+    }
+
+    private Set<MailboxId> listMailboxIdsForRole(MailboxSession session, Role role) throws MailboxException {
+        return systemMailboxesProvider.getMailboxByRole(role, session)
+            .map(MessageManager::getId)
+            .collect(Guavate.toImmutableSet());
+    }
+
     private Stream<MailboxException> updateFlags(MessageId messageId, UpdateMessagePatch updateMessagePatch, MailboxSession mailboxSession, MessageResult messageResult) {
         try {
             if (!updateMessagePatch.isFlagsIdentity()) {
@@ -240,7 +318,7 @@ public class SetMessagesUpdateProcessor implements SetMessagesProcessor {
 
     private SetMessagesResponse.Builder handleMessageUpdateException(MessageId messageId,
                                               SetMessagesResponse.Builder builder,
-                                              MailboxException e) {
+                                              Exception e) {
         LOGGER.error("An error occurred when updating a message", e);
         return builder.notUpdated(ImmutableMap.of(messageId, SetError.builder()
                 .type("anErrorOccurred")

http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/server/protocols/jmap/src/main/java/org/apache/james/jmap/send/MailFactory.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/send/MailFactory.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/send/MailFactory.java
index 3d156d3..966f659 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/send/MailFactory.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/send/MailFactory.java
@@ -40,4 +40,6 @@ public class MailFactory {
             message.getContent());
     }
 
+
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/29d233cc/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessorTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessorTest.java
index de2f809..2264fda 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessorTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesUpdateProcessorTest.java
@@ -49,10 +49,13 @@ public class SetMessagesUpdateProcessorTest {
         MessageIdManager messageIdManager = null;
         SystemMailboxesProvider systemMailboxesProvider = null;
         MailboxId.Factory mailboxIdFactory = null;
+        MessageSender messageSender = null;
         SetMessagesUpdateProcessor sut = new SetMessagesUpdateProcessor(updatePatchConverter,
             messageIdManager,
             systemMailboxesProvider,
-            mailboxIdFactory, new NoopMetricFactory());
+            mailboxIdFactory,
+            messageSender,
+            new NoopMetricFactory());
         SetMessagesRequest requestWithEmptyUpdate = SetMessagesRequest.builder().build();
 
         SetMessagesResponse result = sut.process(requestWithEmptyUpdate, null);
@@ -76,7 +79,16 @@ public class SetMessagesUpdateProcessorTest {
         when(mockConverter.fromJsonNode(any(ObjectNode.class)))
                 .thenReturn(mockInvalidPatch);
 
-        SetMessagesUpdateProcessor sut = new SetMessagesUpdateProcessor(mockConverter, null, null, null, new NoopMetricFactory());
+        MessageIdManager messageIdManager = null;
+        SystemMailboxesProvider systemMailboxesProvider = null;
+        MailboxId.Factory mailboxIdFactory = null;
+        MessageSender messageSender = null;
+        SetMessagesUpdateProcessor sut = new SetMessagesUpdateProcessor(mockConverter,
+            messageIdManager,
+            systemMailboxesProvider,
+            mailboxIdFactory,
+            messageSender,
+            new NoopMetricFactory());
         MessageId requestMessageId = TestMessageId.of(1);
         SetMessagesRequest requestWithInvalidUpdate = SetMessagesRequest.builder()
                 .update(ImmutableMap.of(requestMessageId, JsonNodeFactory.instance.objectNode()))


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org