You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2019/05/05 12:01:35 UTC

[sling-org-apache-sling-committer-cli] branch issue/SLING-8391 updated (74d7415 -> f9cfc67)

This is an automated email from the ASF dual-hosted git repository.

radu pushed a change to branch issue/SLING-8391
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git.


 discard 74d7415  SLING-8391 - Add support for execution modes
     new 31f2224  SLING-8391 - Add support for execution modes
     new f9cfc67  SLING-8391 - Add support for execution modes

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (74d7415)
            \
             N -- N -- N   refs/heads/issue/SLING-8391 (f9cfc67)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/sling/cli/impl/ExecutionContext.java    |  2 +-
 .../impl/{people/Member.java => InputOption.java}  | 49 +++++++-------
 .../java/org/apache/sling/cli/impl/UserInput.java  | 60 +++++++++++++++++
 .../org/apache/sling/cli/impl/mail/Mailer.java     | 25 ++-----
 .../cli/impl/release/PrepareVoteEmailCommand.java  | 77 +++++++++++++++-------
 .../impl/release/PrepareVoteEmailCommandTest.java  | 60 +++++++++--------
 src/test/resources/simplelogger.properties         | 19 ++++++
 7 files changed, 193 insertions(+), 99 deletions(-)
 copy src/main/java/org/apache/sling/cli/impl/{people/Member.java => InputOption.java} (55%)
 create mode 100644 src/main/java/org/apache/sling/cli/impl/UserInput.java
 create mode 100644 src/test/resources/simplelogger.properties


[sling-org-apache-sling-committer-cli] 01/02: SLING-8391 - Add support for execution modes

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch issue/SLING-8391
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git

commit 31f22242890911af290418e847b30089d6dc9a4d
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Fri May 3 15:17:05 2019 +0200

    SLING-8391 - Add support for execution modes
    
    * implemented support for an execution context that commands can use
    to define the way they operate
    * none of the commands actively use the execution mode yet
---
 .../java/org/apache/sling/cli/impl/Command.java    |  2 +-
 .../apache/sling/cli/impl/CommandProcessor.java    | 52 +++++++------
 .../apache/sling/cli/impl/ExecutionContext.java    | 86 ++++++++++++++++++++++
 .../apache/sling/cli/impl/release/ListCommand.java |  3 +-
 .../cli/impl/release/PrepareVoteEmailCommand.java  |  5 +-
 .../sling/cli/impl/release/TallyVotesCommand.java  |  5 +-
 .../cli/impl/release/UpdateLocalSiteCommand.java   |  7 +-
 .../cli/impl/release/UpdateReporterCommand.java    |  7 +-
 .../impl/release/PrepareVoteEmailCommandTest.java  |  3 +-
 .../cli/impl/release/TallyVotesCommandTest.java    |  3 +-
 10 files changed, 135 insertions(+), 38 deletions(-)

diff --git a/src/main/java/org/apache/sling/cli/impl/Command.java b/src/main/java/org/apache/sling/cli/impl/Command.java
index 4b015c4..102b5e6 100644
--- a/src/main/java/org/apache/sling/cli/impl/Command.java
+++ b/src/main/java/org/apache/sling/cli/impl/Command.java
@@ -22,5 +22,5 @@ public interface Command {
     String PROPERTY_NAME_SUBCOMMAND = "subcommand";
     String PROPERTY_NAME_SUMMARY = "summary";
 
-    void execute(String target) throws Exception;
+    void execute(ExecutionContext context) throws Exception;
 }
diff --git a/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java b/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java
index 6ca6bca..cb3de3a 100644
--- a/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java
+++ b/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java
@@ -16,9 +16,6 @@
  */
 package org.apache.sling.cli.impl;
 
-import static org.osgi.service.component.annotations.ReferenceCardinality.MULTIPLE;
-import static org.osgi.service.component.annotations.ReferencePolicy.DYNAMIC;
-
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
@@ -27,20 +24,26 @@ import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
 import org.osgi.framework.launch.Framework;
+import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.osgi.service.component.annotations.ReferenceCardinality.MULTIPLE;
+import static org.osgi.service.component.annotations.ReferencePolicy.DYNAMIC;
+
 @Component(service = CommandProcessor.class)
 public class CommandProcessor {
 
     private final Logger logger = LoggerFactory.getLogger(getClass());
+    private static final String EXEC_ARGS = "exec.args";
     private BundleContext ctx;
 
     private Map<CommandKey, CommandWithProps> commands = new ConcurrentHashMap<>();
 
-    protected void activate(BundleContext ctx) {
+    @Activate
+    private void activate(BundleContext ctx) {
         this.ctx = ctx;
     }
 
@@ -53,17 +56,17 @@ public class CommandProcessor {
         commands.remove(CommandKey.of(props));
     }
 
-    public void runCommand() {
-        // TODO - remove duplication from CLI parsing code
-        CommandKey key = CommandKey.of(ctx.getProperty("exec.args"));
-        String target = parseTarget(ctx.getProperty("exec.args"));
+    void runCommand() {
+        String[] arguments = arguments(ctx.getProperty(EXEC_ARGS));
+        CommandKey key = CommandKey.of(arguments);
+        ExecutionContext context = defineContext(arguments);
         try {
             commands.getOrDefault(key, new CommandWithProps(ignored -> {
                 logger.info("Usage: sling command sub-command [target]");
                 logger.info("");
                 logger.info("Available commands:");
                 commands.forEach((k, c) -> logger.info("{} {}: {}", k.command, k.subCommand, c.summary));
-            }, "")).cmd.execute(target);
+            }, "")).cmd.execute(context);
         } catch (Exception e) {
             logger.warn("Failed running command", e);
         } finally {
@@ -76,15 +79,22 @@ public class CommandProcessor {
         }
     }
 
-    private String parseTarget(String cliSpec) {
-        if (cliSpec == null || cliSpec.isEmpty())
-            return null;
+    private String[] arguments(String cliSpec) {
+        if (cliSpec == null) {
+            return new String[0];
+        }
+        return cliSpec.split(" ");
+    }
 
-        String[] args = cliSpec.split(" ");
-        if (args.length < 3)
+    private ExecutionContext defineContext(String[] arguments) {
+        if (arguments.length < 3)
             return null;
-        
-        return args[2];
+        String target = arguments[2];
+        if (arguments.length > 3) {
+            return new ExecutionContext(target, arguments[3]);
+        } else {
+            return new ExecutionContext(target, null);
+        }
     }
     
 
@@ -95,15 +105,11 @@ public class CommandProcessor {
         private final String command;
         private final String subCommand;
 
-        static CommandKey of(String cliSpec) {
-            if (cliSpec == null || cliSpec.isEmpty())
-                return EMPTY;
-
-            String[] args = cliSpec.split(" ");
-            if (args.length < 2)
+        static CommandKey of(String[] arguments) {
+            if (arguments.length < 2)
                 return EMPTY;
 
-            return new CommandKey(args[0], args[1]);
+            return new CommandKey(arguments[0], arguments[1]);
         }
 
         static CommandKey of(Map<String, ?> serviceProps) {
diff --git a/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java b/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java
new file mode 100644
index 0000000..d2a433e
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java
@@ -0,0 +1,86 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.sling.cli.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Defines the way a {@link Command} will be executed, together with the {@code command}'s execution target.
+ */
+public class ExecutionContext {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ExecutionContext.class);
+
+    private final String target;
+    private final Mode mode;
+
+    /**
+     * Creates an {@code ExecutionContext}.
+     *
+     * @param target the command's target
+     * @param mode   the execution mode
+     */
+    public ExecutionContext(String target, String mode) {
+        this.target = target;
+        if (mode == null) {
+            this.mode = Mode.DRY_RUN;
+        } else {
+            this.mode = Mode.fromString(mode);
+        }
+    }
+
+    /**
+     * Returns the execution target for a command.
+     *
+     * @return the execution target
+     */
+    public String getTarget() {
+        return target;
+    }
+
+    /**
+     * Returns the execution mode for a command.
+     *
+     * @return the execution mode
+     */
+    public Mode getMode() {
+        return mode;
+    }
+
+    enum Mode {
+        DRY_RUN("--dry-run"), INTERACTIVE("--interactive"), AUTO("--auto");
+
+        private final String string;
+
+        Mode(String string) {
+            this.string = string;
+        }
+
+        static Mode fromString(String value) {
+            for (Mode m : values()) {
+                if (m.string.equals(value)) {
+                    return m;
+                }
+            }
+            LOGGER.warn("Unknown command execution mode {}. Switching to default mode {}.", value, DRY_RUN.string);
+            return DRY_RUN;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java b/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java
index 1d35e29..39b9b8d 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/ListCommand.java
@@ -19,6 +19,7 @@ package org.apache.sling.cli.impl.release;
 import java.io.IOException;
 
 import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
@@ -37,7 +38,7 @@ public class ListCommand implements Command {
     private StagingRepositoryFinder repoFinder;
 
     @Override
-    public void execute(String target) {
+    public void execute(ExecutionContext context) {
         try {
             repoFinder.list().stream()
                 .forEach( r -> logger.info("{}\t{}", r.getRepositoryId(), r.getDescription()));
diff --git a/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java b/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
index 9e50205..1862605 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
@@ -23,6 +23,7 @@ import java.util.stream.Collectors;
 import javax.mail.internet.InternetAddress;
 
 import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.jira.Version;
 import org.apache.sling.cli.impl.jira.VersionFinder;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
@@ -88,9 +89,9 @@ public class PrepareVoteEmailCommand implements Command {
             "https://issues.apache.org/jira/browse/SLING/fixforversion/##VERSION_ID##";
 
     @Override
-    public void execute(String target) {
+    public void execute(ExecutionContext context) {
         try {
-            int repoId = Integer.parseInt(target);
+            int repoId = Integer.parseInt(context.getTarget());
             StagingRepository repo = repoFinder.find(repoId);
             List<Release> releases = Release.fromString(repo.getDescription());
             List<Version> versions = releases.stream()
diff --git a/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
index 48a2f69..6e82cfd 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
 import javax.mail.internet.InternetAddress;
 
 import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.mail.Email;
 import org.apache.sling.cli.impl.mail.VoteThreadFinder;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
@@ -78,10 +79,10 @@ public class TallyVotesCommand implements Command {
             "\n";
 
     @Override
-    public void execute(String target) {
+    public void execute(ExecutionContext context) {
         try {
             
-            StagingRepository repository = repoFinder.find(Integer.parseInt(target));
+            StagingRepository repository = repoFinder.find(Integer.parseInt(context.getTarget()));
             List<Release> releases = Release.fromString(repository.getDescription());
             String releaseName = releases.stream().map(Release::getName).collect(Collectors.joining(", "));
             String releaseFullName = releases.stream().map(Release::getFullName).collect(Collectors.joining(", "));
diff --git a/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java b/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
index 99ab368..66aef04 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
@@ -24,6 +24,7 @@ import java.time.LocalDateTime;
 import java.util.List;
 
 import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.jbake.JBakeContentUpdater;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
 import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
@@ -51,14 +52,12 @@ public class UpdateLocalSiteCommand implements Command {
     private final Logger logger = LoggerFactory.getLogger(getClass());
     
     @Override
-    public void execute(String target) {
-        
-        
+    public void execute(ExecutionContext context) {
         try {
             ensureRepo();
             try ( Git git = Git.open(new File(GIT_CHECKOUT)) ) {
                 
-                StagingRepository repository = repoFinder.find(Integer.parseInt(target));
+                StagingRepository repository = repoFinder.find(Integer.parseInt(context.getTarget()));
                 List<Release> releases = Release.fromString(repository.getDescription());
                 
                 JBakeContentUpdater updater = new JBakeContentUpdater();
diff --git a/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java b/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java
index fdf0ef2..04d287c 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java
@@ -39,6 +39,7 @@ import org.apache.http.message.BasicNameValuePair;
 import org.apache.sling.cli.impl.Command;
 import org.apache.sling.cli.impl.Credentials;
 import org.apache.sling.cli.impl.CredentialsService;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
 import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
 import org.osgi.service.component.annotations.Activate;
@@ -67,9 +68,9 @@ public class UpdateReporterCommand implements Command {
     private CredentialsProvider credentialsProvider;
 
     @Override
-    public void execute(String target) {
+    public void execute(ExecutionContext context) {
         try {
-            StagingRepository repository = repoFinder.find(Integer.parseInt(target));
+            StagingRepository repository = repoFinder.find(Integer.parseInt(context.getTarget()));
             
             try (CloseableHttpClient client =
                          HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build()) {
@@ -92,7 +93,7 @@ public class UpdateReporterCommand implements Command {
                 }
             }
         } catch (IOException e) {
-            LOGGER.error(String.format("Unable to update reporter service; passed command: %s.", target), e);
+            LOGGER.error(String.format("Unable to update reporter service; passed command: %s.", context.getTarget()), e);
         }
 
     }
diff --git a/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
index 4e971f2..24a9660 100644
--- a/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
+++ b/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
@@ -19,6 +19,7 @@
 package org.apache.sling.cli.impl.release;
 
 import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.jira.Version;
 import org.apache.sling.cli.impl.jira.VersionFinder;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
@@ -83,7 +84,7 @@ public class PrepareVoteEmailCommandTest {
         ServiceReference<?> reference =
                 osgiContext.bundleContext().getServiceReference(Command.class.getName());
         Command command = (Command) osgiContext.bundleContext().getService(reference);
-        command.execute("123");
+        command.execute(new ExecutionContext("123", null));
         verify(logger).info(
                 "From: John Doe <jo...@apache.org>\n" +
                         "To: \"Sling Developers List\" <de...@sling.apache.org>\n" +
diff --git a/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java
index 29f6012..7800a07 100644
--- a/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java
+++ b/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java
@@ -28,6 +28,7 @@ import javax.mail.internet.InternetAddress;
 import org.apache.sling.cli.impl.Command;
 import org.apache.sling.cli.impl.Credentials;
 import org.apache.sling.cli.impl.CredentialsService;
+import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.mail.Email;
 import org.apache.sling.cli.impl.mail.VoteThreadFinder;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
@@ -110,7 +111,7 @@ public class TallyVotesCommandTest {
         ServiceReference<?> reference =
                 osgiContext.bundleContext().getServiceReference(Command.class.getName());
         Command command = (Command) osgiContext.bundleContext().getService(reference);
-        command.execute("123");
+        command.execute(new ExecutionContext("123", null));
         verify(logger).info(
                 "From: John Doe <jo...@apache.org> \n" +
                 "To: \"Sling Developers List\" <de...@sling.apache.org>\n" +


[sling-org-apache-sling-committer-cli] 02/02: SLING-8391 - Add support for execution modes

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch issue/SLING-8391
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git

commit f9cfc67ab62b91ad9fe67eff9d38992b26c708b0
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Sun May 5 13:54:28 2019 +0200

    SLING-8391 - Add support for execution modes
    
    * implemented the 3 execution modes for `release prepare-email`
---
 .../apache/sling/cli/impl/ExecutionContext.java    |  2 +-
 .../org/apache/sling/cli/impl/InputOption.java     | 63 ++++++++++++++++++
 .../java/org/apache/sling/cli/impl/UserInput.java  | 60 +++++++++++++++++
 .../org/apache/sling/cli/impl/mail/Mailer.java     | 25 ++-----
 .../cli/impl/release/PrepareVoteEmailCommand.java  | 77 +++++++++++++++-------
 .../impl/release/PrepareVoteEmailCommandTest.java  | 60 +++++++++--------
 src/test/resources/simplelogger.properties         | 19 ++++++
 7 files changed, 232 insertions(+), 74 deletions(-)

diff --git a/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java b/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java
index d2a433e..77eac71 100644
--- a/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java
+++ b/src/main/java/org/apache/sling/cli/impl/ExecutionContext.java
@@ -64,7 +64,7 @@ public class ExecutionContext {
         return mode;
     }
 
-    enum Mode {
+    public enum Mode {
         DRY_RUN("--dry-run"), INTERACTIVE("--interactive"), AUTO("--auto");
 
         private final String string;
diff --git a/src/main/java/org/apache/sling/cli/impl/InputOption.java b/src/main/java/org/apache/sling/cli/impl/InputOption.java
new file mode 100644
index 0000000..d2471d9
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/InputOption.java
@@ -0,0 +1,63 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.sling.cli.impl;
+
+public class InputOption {
+
+    private final String text;
+    private final String mnemonic;
+
+    public static final InputOption YES = new InputOption("Yes", "y");
+    public static final InputOption NO = new InputOption("No", "n");
+
+    public InputOption(String text, String mnemonic) {
+        this.text = text;
+        this.mnemonic = mnemonic;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public String getMnemonic() {
+        return mnemonic;
+    }
+
+    @Override
+    public String toString() {
+        return text + " (" + mnemonic + ")";
+    }
+
+    @Override
+    public int hashCode() {
+        return text.hashCode() + mnemonic.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof InputOption) {
+            InputOption other = (InputOption) obj;
+            return text.equals(other.text) && mnemonic.equals(other.mnemonic);
+        }
+        return false;
+    }
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/UserInput.java b/src/main/java/org/apache/sling/cli/impl/UserInput.java
new file mode 100644
index 0000000..38fb7a6
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/UserInput.java
@@ -0,0 +1,60 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.sling.cli.impl;
+
+import java.io.Console;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class UserInput {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(UserInput.class);
+    private static final InputOption[] YES_NO = new InputOption[]{InputOption.YES, InputOption.NO};
+
+    public static InputOption yesNo(String question, InputOption defaultOption) {
+        LOGGER.info(question);
+        Set<InputOption> answers = new LinkedHashSet<>(Arrays.asList(YES_NO));
+        String choice =
+                answers.stream().map(InputOption::toString).collect(Collectors.joining("/")) + "? [" + defaultOption.toString() +
+                        "]: ";
+        while (true) {
+            System.out.print(choice);
+            Console console = System.console();
+            if (console != null) {
+                String answerMnemonic = console.readLine();
+                if ("".equals(answerMnemonic)) {
+                    return defaultOption;
+                }
+                for (InputOption o : YES_NO) {
+                    if (o.getMnemonic().equals(answerMnemonic)) {
+                        return o;
+                    }
+                }
+            } else {
+                throw new IllegalStateException("System console unavailable.");
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/Mailer.java b/src/main/java/org/apache/sling/cli/impl/mail/Mailer.java
index 0c0a4e4..0b5bc61 100644
--- a/src/main/java/org/apache/sling/cli/impl/mail/Mailer.java
+++ b/src/main/java/org/apache/sling/cli/impl/mail/Mailer.java
@@ -18,22 +18,17 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.cli.impl.mail;
 
-import java.io.UnsupportedEncodingException;
+import java.io.ByteArrayInputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Properties;
 
-import javax.mail.Address;
-import javax.mail.Message;
 import javax.mail.MessagingException;
 import javax.mail.Session;
 import javax.mail.Transport;
-import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeMessage;
 
 import org.apache.sling.cli.impl.Credentials;
 import org.apache.sling.cli.impl.CredentialsService;
-import org.apache.sling.cli.impl.people.Member;
-import org.apache.sling.cli.impl.people.MembersFinder;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
@@ -57,23 +52,15 @@ public class Mailer {
     @Reference
     private CredentialsService credentialsService;
 
-    @Reference
-    private MembersFinder membersFinder;
-
-    public void send(String to, String subject, String body) {
+    public void send(String source) {
         Properties properties = new Properties(SMTP_PROPERTIES);
         Session session = Session.getInstance(properties);
         try {
-            MimeMessage message = new MimeMessage(session);
-            Member sender = membersFinder.getCurrentMember();
+            MimeMessage message = new MimeMessage(session, new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)));
             Credentials credentials = credentialsService.getCredentials();
-            message.setFrom(new InternetAddress(sender.getEmail(), sender.getEmail(), StandardCharsets.UTF_8.name()));
-            message.setSubject(subject);
-            message.setText(body, StandardCharsets.UTF_8.name());
-            message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
-            Transport.send(message, new Address[] {new InternetAddress(to)}, credentials.getUsername(), credentials.getPassword());
-        } catch (MessagingException | UnsupportedEncodingException e) {
-            LOGGER.error(String.format("Unable to send email with Subject '%s' to '%s'.", subject, to), e);
+            Transport.send(message, credentials.getUsername(), credentials.getPassword());
+        } catch (MessagingException e) {
+            LOGGER.error(String.format("Unable to send the following email:\n%s", source), e);
         }
 
     }
diff --git a/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java b/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
index 1862605..5e3f429 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
@@ -24,8 +24,11 @@ import javax.mail.internet.InternetAddress;
 
 import org.apache.sling.cli.impl.Command;
 import org.apache.sling.cli.impl.ExecutionContext;
+import org.apache.sling.cli.impl.InputOption;
+import org.apache.sling.cli.impl.UserInput;
 import org.apache.sling.cli.impl.jira.Version;
 import org.apache.sling.cli.impl.jira.VersionFinder;
+import org.apache.sling.cli.impl.mail.Mailer;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
 import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
 import org.apache.sling.cli.impl.people.Member;
@@ -52,6 +55,9 @@ public class PrepareVoteEmailCommand implements Command {
     @Reference
     private VersionFinder versionFinder;
 
+    @Reference
+    private Mailer mailer;
+
     // TODO - replace with file template
     private static final String EMAIL_TEMPLATE =
             "From: ##FROM##\n" +
@@ -61,31 +67,30 @@ public class PrepareVoteEmailCommand implements Command {
             "Hi,\n" + 
             "\n" + 
             "We solved ##FIXED_ISSUES_COUNT## issue(s) in ##RELEASE_OR_RELEASES##:\n" +
-            "\n" + 
             "##RELEASE_JIRA_LINKS##\n" +
-            "\n" + 
-            "Staging repository:\n" + 
-            "https://repository.apache.org/content/repositories/orgapachesling-##RELEASE_ID##/\n" + 
-            "\n" + 
-            "You can use this UNIX script to download the release and verify the signatures:\n" + 
-            "https://gitbox.apache.org/repos/asf?p=sling-tooling-release.git;a=blob;f=check_staged_release.sh;hb=HEAD\n" + 
-            "\n" + 
-            "Usage:\n" + 
-            "sh check_staged_release.sh ##RELEASE_ID## /tmp/sling-staging\n" + 
-            "\n" + 
-            "Please vote to approve this release:\n" + 
-            "\n" + 
-            "  [ ] +1 Approve the release\n" + 
-            "  [ ]  0 Don't care\n" + 
-            "  [ ] -1 Don't release, because ...\n" + 
-            "\n" + 
+            "\n" +
+            "Staging repository:\n" +
+            "https://repository.apache.org/content/repositories/orgapachesling-##RELEASE_ID##/\n" +
+            "\n" +
+            "You can use this UNIX script to download the release and verify the signatures:\n" +
+            "https://gitbox.apache.org/repos/asf?p=sling-tooling-release.git;a=blob;f=check_staged_release.sh;hb=HEAD\n" +
+            "\n" +
+            "Usage:\n" +
+            "sh check_staged_release.sh ##RELEASE_ID## /tmp/sling-staging\n" +
+            "\n" +
+            "Please vote to approve this release:\n" +
+            "\n" +
+            "  [ ] +1 Approve the release\n" +
+            "  [ ]  0 Don't care\n" +
+            "  [ ] -1 Don't release, because ...\n" +
+            "\n" +
             "This majority vote is open for at least 72 hours.\n" +
             "\n" +
             "Regards,\n" +
             "##USER_NAME##\n" +
             "\n";
 
-    private static final String RELEASE_TEMPLATE = 
+    private static final String RELEASE_TEMPLATE =
             "https://issues.apache.org/jira/browse/SLING/fixforversion/##VERSION_ID##";
 
     @Override
@@ -97,19 +102,19 @@ public class PrepareVoteEmailCommand implements Command {
             List<Version> versions = releases.stream()
                     .map( r -> versionFinder.find(r.getName()))
                     .collect(Collectors.toList());
-            
+
             String releaseName = releases.stream()
                     .map( Release::getFullName )
                     .collect(Collectors.joining(", "));
-            
+
             int fixedIssueCounts = versions.stream().mapToInt( Version::getIssuesFixedCount).sum();
             String releaseOrReleases = versions.size() > 1 ?
                     "these releases" : "this release";
-            
+
             String releaseJiraLinks = versions.stream()
                 .map( v -> RELEASE_TEMPLATE.replace("##VERSION_ID##", String.valueOf(v.getId())))
                 .collect(Collectors.joining("\n"));
-                
+
             Member currentMember = membersFinder.getCurrentMember();
             String emailContents = EMAIL_TEMPLATE
                     .replace("##FROM##", new InternetAddress(currentMember.getEmail(), currentMember.getName()).toString())
@@ -119,9 +124,31 @@ public class PrepareVoteEmailCommand implements Command {
                     .replace("##RELEASE_JIRA_LINKS##", releaseJiraLinks)
                     .replace("##FIXED_ISSUES_COUNT##", String.valueOf(fixedIssueCounts))
                     .replace("##USER_NAME##", currentMember.getName());
-                    
-            LOGGER.info(emailContents);
-
+            switch (context.getMode()) {
+                case DRY_RUN:
+                    LOGGER.info("The following email would be sent from your @apache.org address (see the \"From:\" header):\n");
+                    LOGGER.info(emailContents);
+                    break;
+                case INTERACTIVE:
+                    StringBuilder question = new StringBuilder("Should the following email be sent from your @apache.org address (see the" +
+                            " \"From:\" header)?\n\n");
+                    question.append(emailContents);
+                    InputOption answer = UserInput.yesNo(question.toString(), InputOption.YES);
+                    if (InputOption.YES.equals(answer)) {
+                        LOGGER.info("Sending email...");
+                        mailer.send(emailContents);
+                        LOGGER.info("Done!");
+                    } else if (InputOption.NO.equals(answer)) {
+                        LOGGER.info("Aborted.");
+                    }
+                    break;
+                case AUTO:
+                    LOGGER.info(emailContents);
+                    LOGGER.info("Sending email...");
+                    mailer.send(emailContents);
+                    LOGGER.info("Done!");
+                    break;
+            }
         } catch (IOException e) {
             LOGGER.warn("Failed executing command", e);
         }
diff --git a/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
index 24a9660..653cd47 100644
--- a/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
+++ b/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
@@ -18,10 +18,13 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.cli.impl.release;
 
+import java.io.IOException;
+
 import org.apache.sling.cli.impl.Command;
 import org.apache.sling.cli.impl.ExecutionContext;
 import org.apache.sling.cli.impl.jira.Version;
 import org.apache.sling.cli.impl.jira.VersionFinder;
+import org.apache.sling.cli.impl.mail.Mailer;
 import org.apache.sling.cli.impl.nexus.StagingRepository;
 import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
 import org.apache.sling.cli.impl.people.Member;
@@ -34,16 +37,13 @@ import org.osgi.framework.ServiceReference;
 import org.powermock.core.classloader.annotations.PowerMockIgnore;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
 
 @RunWith(PowerMockRunner.class)
-@PrepareForTest({PrepareVoteEmailCommand.class, LoggerFactory.class})
+@PrepareForTest({PrepareVoteEmailCommand.class})
 @PowerMockIgnore({
                          // https://github.com/powermock/powermock/issues/864
                          "com.sun.org.apache.xerces.*",
@@ -57,35 +57,16 @@ public class PrepareVoteEmailCommandTest {
 
     @Test
     public void testPrepareEmailGeneration() throws Exception {
-        mockStatic(LoggerFactory.class);
-        Logger logger = mock(Logger.class);
-        when(LoggerFactory.getLogger(PrepareVoteEmailCommand.class)).thenReturn(logger);
-        MembersFinder membersFinder = mock(MembersFinder.class);
-        when(membersFinder.getCurrentMember()).thenReturn(new Member("johndoe", "John Doe", true));
-
-        StagingRepository stagingRepository = mock(StagingRepository.class);
-        when(stagingRepository.getDescription()).thenReturn("Apache Sling CLI Test 1.0.0");
-        StagingRepositoryFinder stagingRepositoryFinder = mock(StagingRepositoryFinder.class);
-        when(stagingRepositoryFinder.find(123)).thenReturn(stagingRepository);
-
-        VersionFinder versionFinder = mock(VersionFinder.class);
-        Version version = mock(Version.class);
-        when(version.getName()).thenReturn("CLI Test 1.0.0");
-        when(version.getId()).thenReturn(1);
-        when(version.getIssuesFixedCount()).thenReturn(42);
-        when(versionFinder.find("CLI Test 1.0.0")).thenReturn(version);
-
-        osgiContext.registerService(MembersFinder.class, membersFinder);
-        osgiContext.registerService(StagingRepositoryFinder.class, stagingRepositoryFinder);
-        osgiContext.registerService(VersionFinder.class, versionFinder);
-
+        Mailer mailer = mock(Mailer.class);
+        prepareExecution(mailer);
+        ExecutionContext context = new ExecutionContext("123", "--auto");
         osgiContext.registerInjectActivateService(new PrepareVoteEmailCommand());
 
         ServiceReference<?> reference =
                 osgiContext.bundleContext().getServiceReference(Command.class.getName());
         Command command = (Command) osgiContext.bundleContext().getService(reference);
-        command.execute(new ExecutionContext("123", null));
-        verify(logger).info(
+        command.execute(context);
+        verify(mailer).send(
                 "From: John Doe <jo...@apache.org>\n" +
                         "To: \"Sling Developers List\" <de...@sling.apache.org>\n" +
                         "Subject: [VOTE] Release Apache Sling CLI Test 1.0.0\n" +
@@ -93,7 +74,6 @@ public class PrepareVoteEmailCommandTest {
                         "Hi,\n" +
                         "\n" +
                         "We solved 42 issue(s) in this release:\n" +
-                        "\n" +
                         "https://issues.apache.org/jira/browse/SLING/fixforversion/1\n" +
                         "\n" +
                         "Staging repository:\n" +
@@ -117,4 +97,26 @@ public class PrepareVoteEmailCommandTest {
                         "John Doe\n" +
                         "\n");
     }
+
+    private void prepareExecution(Mailer mailer) throws IOException {
+        MembersFinder membersFinder = mock(MembersFinder.class);
+        when(membersFinder.getCurrentMember()).thenReturn(new Member("johndoe", "John Doe", true));
+
+        StagingRepository stagingRepository = mock(StagingRepository.class);
+        when(stagingRepository.getDescription()).thenReturn("Apache Sling CLI Test 1.0.0");
+        StagingRepositoryFinder stagingRepositoryFinder = mock(StagingRepositoryFinder.class);
+        when(stagingRepositoryFinder.find(123)).thenReturn(stagingRepository);
+
+        VersionFinder versionFinder = mock(VersionFinder.class);
+        Version version = mock(Version.class);
+        when(version.getName()).thenReturn("CLI Test 1.0.0");
+        when(version.getId()).thenReturn(1);
+        when(version.getIssuesFixedCount()).thenReturn(42);
+        when(versionFinder.find("CLI Test 1.0.0")).thenReturn(version);
+
+        osgiContext.registerService(MembersFinder.class, membersFinder);
+        osgiContext.registerService(StagingRepositoryFinder.class, stagingRepositoryFinder);
+        osgiContext.registerService(VersionFinder.class, versionFinder);
+        osgiContext.registerService(Mailer.class, mailer);
+    }
 }
diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties
new file mode 100644
index 0000000..620f7c4
--- /dev/null
+++ b/src/test/resources/simplelogger.properties
@@ -0,0 +1,19 @@
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# 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.
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+org.slf4j.simpleLogger.defaultLogLevel=warn