JSIEVE-103 Implement vacation command
diff --git a/core/src/main/java/org/apache/jsieve/CommandStateManager.java b/core/src/main/java/org/apache/jsieve/CommandStateManager.java
index e4eda37..43ca621 100644
--- a/core/src/main/java/org/apache/jsieve/CommandStateManager.java
+++ b/core/src/main/java/org/apache/jsieve/CommandStateManager.java
@@ -46,6 +46,11 @@
private boolean fieldHasActions = false;
/**
+ * Did vacation instruction already executed for this script ?
+ */
+ private boolean vacationProcessed = false;
+
+ /**
* Constructor for CommandStateManager.
*/
public CommandStateManager() {
@@ -139,4 +144,20 @@
fieldImplicitKeep = implicitKeep;
}
+ /**
+ * Returns whether a vacation command was processed.
+ *
+ * RFC 5230 section 4.7 : Vacation can only be executed once per script. A script MUST fail
+ * with an appropriate error if it attempts to execute two or more
+ * vacation actions.
+ *
+ * @return false if no vacation command was executed before in this script, false otherwise
+ */
+ public boolean getVacationProcessed() {
+ return vacationProcessed;
+ }
+
+ public void setVacationProcessed(boolean vacationProcessed) {
+ this.vacationProcessed = vacationProcessed;
+ }
}
diff --git a/core/src/main/java/org/apache/jsieve/commands/optional/Vacation.java b/core/src/main/java/org/apache/jsieve/commands/optional/Vacation.java
new file mode 100644
index 0000000..fc54d59
--- /dev/null
+++ b/core/src/main/java/org/apache/jsieve/commands/optional/Vacation.java
@@ -0,0 +1,96 @@
+/****************************************************************
+ * 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.jsieve.commands.optional;
+
+import org.apache.jsieve.Arguments;
+import org.apache.jsieve.Block;
+import org.apache.jsieve.SieveContext;
+import org.apache.jsieve.commands.AbstractActionCommand;
+import org.apache.jsieve.exception.CommandException;
+import org.apache.jsieve.exception.SieveException;
+import org.apache.jsieve.mail.MailAdapter;
+import org.apache.jsieve.mail.optional.ActionVacation;
+import org.apache.jsieve.utils.ArgumentParser;
+
+/**
+ * See https://tools.ietf.org/html/rfc5230
+ */
+public class Vacation extends AbstractActionCommand {
+
+ public static final String DAYS = ":days";
+ public static final String DAYS_EXCEPTION_MESSAGE = "Expecting a number argument setting the number of days after tag " + DAYS;
+ public static final String SUBJECT = ":subject";
+ public static final String SUBJECT_EXCEPTION_MESSAGE = "Expecting a string argument setting the subject after tag " + SUBJECT;
+ public static final String FROM = ":from";
+ public static final String FROM_EXCEPTION_MESSAGE = "Expecting a string argument setting the from field of sent mails after tag " + FROM;
+ public static final String ADDRESSES = ":addresses";
+ public static final String ADDRESSES_EXCEPTION_MESSAGE = "Expecting a string list argument setting the additional addresses this script is authorized to respond to after tag " + ADDRESSES;
+ public static final String MIME = ":mime";
+ public static final String MIME_EXCEPTION_MESSAGE = "Expecting a string argument setting a mime message instead of the reason string after tag " + MIME;
+ public static final String HANDLE = ":handle";
+ public static final String HANDLE_EXCEPTION_MESSAGE = "Expecting a string argument setting handle string after tag " + HANDLE;
+
+ @Override
+ protected Object executeBasic(MailAdapter mail, Arguments arguments, Block block, SieveContext context) throws SieveException {
+ mail.addAction(retrieveAction(arguments));
+ return null;
+ }
+
+ @Override
+ protected void validateState(SieveContext context) throws CommandException {
+ // RFC-5230 Section 4.7 : The vacation action is incompatible with the Sieve reject and refuse actions
+ super.validateState(context);
+ // RFC-5230 Section 4.7 : Vacation can only be executed once per script
+ if (context.getCommandStateManager().getVacationProcessed())
+ throw context.getCoordinate()
+ .commandException("The \"vacation\" command is not allowed to be executed after other vacation commands");
+ }
+
+ @Override
+ protected void updateState(SieveContext context) {
+ context.getCommandStateManager().setHasActions(true);
+ context.getCommandStateManager().setInProlog(false);
+ // RFC-5230 Section 4.7 : Vacation does not affect Sieve's implicit keep action.
+ context.getCommandStateManager().setVacationProcessed(true);
+ context.getCommandStateManager().setImplicitKeep(context.getCommandStateManager().isImplicitKeep());
+ }
+
+ @Override
+ protected void validateArguments(Arguments arguments, SieveContext context) throws SieveException {
+ retrieveAction(arguments);
+ }
+
+ private ActionVacation retrieveAction(Arguments arguments) throws SieveException {
+ ArgumentParser argumentParser = new ArgumentParser(arguments.getArgumentList());
+ argumentParser.throwOnUnvalidSeenSingleTag();
+ argumentParser.throwOnUnvalidSeenTagWithValue(FROM, SUBJECT, HANDLE, MIME, DAYS, ADDRESSES);
+
+ return ActionVacation.builder()
+ .addresses(argumentParser.getStringListForTag(ADDRESSES, ADDRESSES_EXCEPTION_MESSAGE))
+ .duration(argumentParser.getNumericValueForTag(DAYS, DAYS_EXCEPTION_MESSAGE))
+ .handle(argumentParser.getStringValueForTag(HANDLE, HANDLE_EXCEPTION_MESSAGE))
+ .mime(argumentParser.getStringValueForTag(MIME, MIME_EXCEPTION_MESSAGE))
+ .subject(argumentParser.getStringValueForTag(SUBJECT, SUBJECT_EXCEPTION_MESSAGE))
+ .from(argumentParser.getStringValueForTag(FROM, FROM_EXCEPTION_MESSAGE))
+ .reason(argumentParser.getRemainingStringValue("Expecting a single String value as a reason"))
+ .build();
+ }
+
+}
diff --git a/core/src/main/java/org/apache/jsieve/mail/optional/ActionVacation.java b/core/src/main/java/org/apache/jsieve/mail/optional/ActionVacation.java
new file mode 100644
index 0000000..9113cd7
--- /dev/null
+++ b/core/src/main/java/org/apache/jsieve/mail/optional/ActionVacation.java
@@ -0,0 +1,206 @@
+/****************************************************************
+ * 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.jsieve.mail.optional;
+
+import org.apache.jsieve.exception.SyntaxException;
+import org.apache.jsieve.mail.Action;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class ActionVacation encapsulates the information required to reject a mail.
+ * See RFC 5230, Section 4.1.
+ */
+public class ActionVacation implements Action {
+
+ public static class ActionVacationBuilder {
+
+ private static final int SITE_DEFINED_DEFAULT_VACATION_DURATION = 7;
+ private static final int MINIMUM_VACATION_DURATION = 1;
+
+ private String subject;
+ private String from;
+ private List<String> addresses = new ArrayList<String>();
+ private String handle;
+ private String reason;
+ private String mime;
+ private Integer duration;
+
+ public ActionVacationBuilder() {
+ duration = SITE_DEFINED_DEFAULT_VACATION_DURATION;
+ }
+
+ public ActionVacationBuilder subject(String subject) {
+ this.subject = subject;
+ return this;
+ }
+
+ public ActionVacationBuilder from(String from) {
+ this.from = from;
+ return this;
+ }
+
+ public ActionVacationBuilder addresses(List<String> addresses) {
+ this.addresses = addresses;
+ return this;
+ }
+
+ public ActionVacationBuilder handle(String handle) {
+ this.handle = handle;
+ return this;
+ }
+
+ public ActionVacationBuilder reason(String reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ public ActionVacationBuilder mime(String mime) {
+ this.mime = mime;
+ return this;
+ }
+
+ public ActionVacationBuilder duration(Integer duration) {
+ this.duration = duration;
+ return this;
+ }
+
+ public ActionVacation build() throws SyntaxException {
+ if (!eitherReasonOrMime()) {
+ throw new SyntaxException("vacation need you to set you either the reason string or a MIME message after tag :mime");
+ }
+ return new ActionVacation(subject, from, addresses, reason, computeDuration(duration), handle, mime);
+ }
+
+ private int computeDuration(Integer duration) {
+ if (duration == null) {
+ return SITE_DEFINED_DEFAULT_VACATION_DURATION;
+ }
+ if (duration < MINIMUM_VACATION_DURATION) {
+ return MINIMUM_VACATION_DURATION;
+ } else {
+ return duration;
+ }
+ }
+
+ private boolean eitherReasonOrMime() {
+ return (reason == null) ^ (mime == null);
+ }
+ }
+
+ public static ActionVacationBuilder builder() {
+ return new ActionVacationBuilder();
+ }
+
+ private final String subject;
+ private final String from;
+ private final List<String> addresses;
+ private final String handle;
+ private final String reason;
+ private final String mime;
+ private final int duration;
+
+ private ActionVacation(String subject, String from, List<String> addresses, String reason, int duration, String handle, String mime) {
+ this.subject = subject;
+ this.from = from;
+ this.addresses = Collections.unmodifiableList(addresses);
+ this.reason = reason;
+ this.duration = duration;
+ this.handle = handle;
+ this.mime = mime;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public List<String> getAddresses() {
+ return addresses;
+ }
+
+ public String getHandle() {
+ return handle;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public String getMime() {
+ return mime;
+ }
+
+ public String toString() {
+ return "Action: " + getClass().getName();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ActionVacation that = (ActionVacation) o;
+
+ return duration == that.duration
+ && equalsNullProtected(this.subject, subject)
+ && equalsNullProtected(this.from, from)
+ && equalsNullProtected(this.handle, handle)
+ && equalsNullProtected(this.reason, reason)
+ && equalsNullProtected(this.mime, mime);
+ }
+
+ private boolean equalsNullProtected(Object object1, Object object2) {
+ if (object1 == null) {
+ return object2 == null;
+ }
+ return object1.equals(object2);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = hashCodeNullProtected(subject);
+ result = 31 * result + hashCodeNullProtected(from);
+ result = 31 * result + hashCodeNullProtected(handle);
+ result = 31 * result + hashCodeNullProtected(reason);
+ result = 31 * result + hashCodeNullProtected(mime);
+ result = 31 * result + hashCodeNullProtected(subject);
+ result = 31 * result + duration;
+ return result;
+ }
+
+ public int hashCodeNullProtected(Object object) {
+ if (object == null) {
+ return 0;
+ }
+ return object.hashCode();
+ }
+
+}
diff --git a/core/src/test/java/org/apache/jsieve/VacationTest.java b/core/src/test/java/org/apache/jsieve/VacationTest.java
new file mode 100644
index 0000000..afe3f40
--- /dev/null
+++ b/core/src/test/java/org/apache/jsieve/VacationTest.java
@@ -0,0 +1,235 @@
+/****************************************************************
+ * 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.jsieve;
+
+import org.apache.jsieve.exception.SyntaxException;
+import org.apache.jsieve.mail.ActionKeep;
+import org.apache.jsieve.mail.optional.ActionVacation;
+import org.apache.jsieve.utils.JUnitUtils;
+import org.apache.jsieve.utils.SieveMailAdapter;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class VacationTest {
+
+ private SieveMailAdapter sieveMailAdapter;
+
+ @Before
+ public void setUp() {
+ sieveMailAdapter = mock(SieveMailAdapter.class);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void NoArgumentsShouldThrow() throws Exception {
+ String script = "vacation;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void unknownTagsShouldThrow() throws Exception {
+ String script = "vacation :unknown;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void uncompletedDaysTagShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :days;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void daysTagWithStringShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :days \"string\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void uncompletedSubjectTagShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :subject;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void uncompletedFromTagShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :from;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void uncompletedAddressesTagShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :addresses;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void uncompletedHandleTagShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :handle;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void uncompletedMimeTagShouldThrow() throws Exception {
+ String script = "vacation :mime;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test
+ public void vacationShouldWork() throws Exception {
+ String script = "vacation \"reason\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @Test
+ public void daysShouldWork() throws Exception {
+ String script = "vacation \"reason\" :days 3;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").duration(3).build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @Test
+ public void subjectShouldWork() throws Exception {
+ String script = "vacation \"reason\" :subject \"any\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").subject("any").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @Test
+ public void fromShouldWork() throws Exception {
+ String script = "vacation \"reason\" :from \"benwa@apache.org\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").from("benwa@apache.org").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void addressesShouldWork() throws Exception {
+ String script = "vacation \"reason\" :addresses \"benwa@apache.org\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @Test
+ public void mimeShouldWork() throws Exception {
+ String script = "vacation :mime \"reason\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().mime("reason").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @Test
+ public void handleShouldWork() throws Exception {
+ String script = "vacation \"reason\" :handle \"plop\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").handle("plop").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void reasonAndMimeTogetherShouldThrow() throws Exception {
+ String script = "vacation \"reason\" :mime \"plop\";";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test(expected = SyntaxException.class)
+ public void reasonAsStringListWithMultipleEntriesShouldThrow() throws Exception {
+ String script = "vacation [\"reason\",\"too much\"];";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+
+ @Test(expected = SyntaxException.class)
+ public void aNumberAsReasonShouldThrow() throws Exception {
+ String script = "vacation 3;";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+ }
+
+ @Test
+ public void reasonAsStringListWithOneEntryShouldWork() throws Exception {
+ String script = "vacation [\"reason\"];";
+
+ JUnitUtils.interpret(sieveMailAdapter, script);
+
+ verify(sieveMailAdapter, times(2)).setContext(any(SieveContext.class));
+ verify(sieveMailAdapter).addAction(ActionVacation.builder().reason("reason").build());
+ verify(sieveMailAdapter, times(2)).addAction(any(ActionKeep.class));
+ verify(sieveMailAdapter).executeActions();
+ verifyNoMoreInteractions(sieveMailAdapter);
+ }
+
+}
diff --git a/core/src/test/resources/org/apache/jsieve/commandsmap.properties b/core/src/test/resources/org/apache/jsieve/commandsmap.properties
index 3dba4a1..6250404 100644
--- a/core/src/test/resources/org/apache/jsieve/commandsmap.properties
+++ b/core/src/test/resources/org/apache/jsieve/commandsmap.properties
@@ -29,6 +29,8 @@
# RFC3082 - Implementations SHOULD support these
reject=org.apache.jsieve.commands.optional.Reject
fileinto=org.apache.jsieve.commands.optional.FileInto
+# Extension RFC 5230 MUST support these
+vacation=org.apache.jsieve.commands.optional.Vacation
# JUnit Commands for Testing
throwtestexception=org.apache.jsieve.commands.ThrowTestException
# Extension Commands