JSIEVE-103 VacationActions should be handled by the mailet sub project
diff --git a/mailet/pom.xml b/mailet/pom.xml index ff80cad..a26032a 100644 --- a/mailet/pom.xml +++ b/mailet/pom.xml
@@ -84,7 +84,11 @@ <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> - + <dependency> + <groupId>joda-time</groupId> + <artifactId>joda-time</artifactId> + <version>2.9.2</version> + </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId>
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/ActionContext.java b/mailet/src/main/java/org/apache/jsieve/mailet/ActionContext.java index 33a94f4..c719009 100644 --- a/mailet/src/main/java/org/apache/jsieve/mailet/ActionContext.java +++ b/mailet/src/main/java/org/apache/jsieve/mailet/ActionContext.java
@@ -19,18 +19,35 @@ package org.apache.jsieve.mailet; import java.util.Collection; +import java.util.Date; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import org.apache.commons.logging.Log; import org.apache.mailet.MailAddress; +import org.joda.time.DateTime; /** * Provides context for action execution. */ public interface ActionContext { - + + /** + * @return Date the script was activated + */ + DateTime getScriptActivationDate(); + + /** + * @return Date the script is currently interpreted + */ + DateTime getScriptInterpretationDate(); + + /** + * @return Recipient receiving the given eMail + */ + MailAddress getRecipient(); + /** * Gets the log. * @return not null
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/ActionDispatcher.java b/mailet/src/main/java/org/apache/jsieve/mailet/ActionDispatcher.java index 0b55bbf..fed4bb4 100644 --- a/mailet/src/main/java/org/apache/jsieve/mailet/ActionDispatcher.java +++ b/mailet/src/main/java/org/apache/jsieve/mailet/ActionDispatcher.java
@@ -20,6 +20,7 @@ package org.apache.jsieve.mailet; import org.apache.jsieve.mail.*; +import org.apache.jsieve.mail.optional.ActionVacation; import org.apache.mailet.Mail; import javax.mail.MessagingException; @@ -83,6 +84,7 @@ actionMap.put(ActionKeep.class, new KeepAction()); actionMap.put(ActionRedirect.class, new RedirectAction()); actionMap.put(ActionReject.class, new RejectAction()); + actionMap.put(ActionVacation.class, new VacationAction()); return actionMap; }
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/ResourceLocator.java b/mailet/src/main/java/org/apache/jsieve/mailet/ResourceLocator.java index 12f4bc0..64ee54d 100644 --- a/mailet/src/main/java/org/apache/jsieve/mailet/ResourceLocator.java +++ b/mailet/src/main/java/org/apache/jsieve/mailet/ResourceLocator.java
@@ -18,6 +18,8 @@ ****************************************************************/ package org.apache.jsieve.mailet; +import org.joda.time.DateTime; + import java.io.InputStream; /** @@ -54,12 +56,37 @@ * </p> */ public interface ResourceLocator { - + + class UserSieveInformation { + private DateTime scriptActivationDate; + private DateTime scriptInterpretationDate; + private InputStream scriptContent; + + public UserSieveInformation(DateTime scriptActivationDate, DateTime scriptInterpretationDate, InputStream scriptContent) { + this.scriptActivationDate = scriptActivationDate; + this.scriptInterpretationDate = scriptInterpretationDate; + this.scriptContent = scriptContent; + } + + public DateTime getScriptActivationDate() { + return scriptActivationDate; + } + + public DateTime getScriptInterpretationDate() { + return scriptInterpretationDate; + } + + public InputStream getScriptContent() { + return scriptContent; + } + } + /** * GET verb locates and loads a resource. * @param uri identifies the Sieve script * @return not null * @throws Exception when the resource cannot be located */ - InputStream get(String uri) throws Exception; + UserSieveInformation get(String uri) throws Exception; + }
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailAdapter.java b/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailAdapter.java index 89586f5..93389f1 100644 --- a/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailAdapter.java +++ b/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailAdapter.java
@@ -40,6 +40,7 @@ import org.apache.mailet.Mail; import org.apache.mailet.MailAddress; import org.apache.mailet.MailetContext; +import org.joda.time.DateTime; import javax.mail.Header; import javax.mail.MessagingException; @@ -81,6 +82,9 @@ private final ActionDispatcher dispatcher; private final Poster poster; + private final DateTime scriptActivationDate; + private final DateTime scriptInterpretationDate; + private final MailAddress recipient; /** * Constructor for SieveMailAdapter. @@ -88,15 +92,30 @@ * @param aMail * @param aMailetContext */ - public SieveMailAdapter(final Mail aMail, final MailetContext aMailetContext, final ActionDispatcher dispatcher, final Poster poster) + public SieveMailAdapter(final Mail aMail, final MailetContext aMailetContext, final ActionDispatcher dispatcher, final Poster poster, + DateTime scriptActivationDate, DateTime scriptInterpretationDate, MailAddress recipient) { this.poster = poster; this.dispatcher = dispatcher; + this.scriptInterpretationDate = scriptInterpretationDate; + this.scriptActivationDate = scriptActivationDate; + this.recipient = recipient; setMail(aMail); setMailetContext(aMailetContext); } - - + + public DateTime getScriptActivationDate() { + return scriptActivationDate; + } + + public DateTime getScriptInterpretationDate() { + return scriptInterpretationDate; + } + + public MailAddress getRecipient() { + return recipient; + } + public void setLog(Log log) { this.log = log; }
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailboxMailet.java b/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailboxMailet.java index 0366b3c..5d6d214 100644 --- a/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailboxMailet.java +++ b/mailet/src/main/java/org/apache/jsieve/mailet/SieveMailboxMailet.java
@@ -19,25 +19,18 @@ package org.apache.jsieve.mailet; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.io.UnsupportedEncodingException; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; -import java.util.Scanner; import java.util.Vector; -import javax.activation.DataHandler; import javax.mail.Header; import javax.mail.MessagingException; import javax.mail.internet.InternetHeaders; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; -import javax.mail.util.ByteArrayDataSource; import org.apache.commons.logging.Log; import org.apache.jsieve.ConfigurationManager; @@ -345,8 +338,8 @@ protected void sieveMessage(MailAddress recipient, Mail aMail) throws MessagingException { String username = getUsername(recipient); try { - final InputStream ins = locator.get(getScriptUri(recipient)); - sieveMessageEvaluate(recipient, aMail, ins); + final ResourceLocator.UserSieveInformation userSieveInformation = locator.get(getScriptUri(recipient)); + sieveMessageEvaluate(recipient, aMail, userSieveInformation); } catch (Exception ex) { // SIEVE is a mail filtering protocol. // Rejecting the mail because it cannot be filtered @@ -359,17 +352,18 @@ } } - private void sieveMessageEvaluate(MailAddress recipient, Mail aMail, InputStream ins) throws MessagingException, IOException { + private void sieveMessageEvaluate(MailAddress recipient, Mail aMail, ResourceLocator.UserSieveInformation userSieveInformation) throws MessagingException, IOException { try { SieveMailAdapter aMailAdapter = new SieveMailAdapter(aMail, - getMailetContext(), actionDispatcher, poster); + getMailetContext(), actionDispatcher, poster, userSieveInformation.getScriptActivationDate(), + userSieveInformation.getScriptInterpretationDate(), recipient); aMailAdapter.setLog(log); // This logging operation is potentially costly if (verbose) { log("Evaluating " + aMailAdapter.toString() + "against \"" + getScriptUri(recipient) + "\""); } - factory.evaluate(aMailAdapter, factory.parse(ins)); + factory.evaluate(aMailAdapter, factory.parse(userSieveInformation.getScriptContent())); } catch (SieveException ex) { handleFailure(recipient, aMail, ex); }
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/VacationAction.java b/mailet/src/main/java/org/apache/jsieve/mailet/VacationAction.java new file mode 100644 index 0000000..3a1abba --- /dev/null +++ b/mailet/src/main/java/org/apache/jsieve/mailet/VacationAction.java
@@ -0,0 +1,100 @@ +/**************************************************************** + * 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.mailet; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.jsieve.mail.Action; +import org.apache.jsieve.mail.optional.ActionVacation; +import org.apache.mailet.Mail; +import org.apache.mailet.MailAddress; +import org.joda.time.Days; + +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import java.util.Date; +import java.util.Enumeration; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class VacationAction implements MailAction { + + @Override + public void execute(Action action, Mail mail, ActionContext context) throws MessagingException { + ActionVacation actionVacation = (ActionVacation) action; + int dayDifference = Days.daysBetween(context.getScriptActivationDate(), context.getScriptInterpretationDate()).getDays(); + if (isStillInVacation(actionVacation, dayDifference)) { + if (isValidForReply(mail, actionVacation, context)) { + if (!isMailingList(mail)) { + sendVacationNotification(mail, actionVacation, context); + } + } + } + } + + private void sendVacationNotification(Mail mail, ActionVacation actionVacation, ActionContext context) throws MessagingException { + VacationReply vacationReply = VacationReply.builder(mail, context) + .from(actionVacation.getFrom()) + .mime(actionVacation.getMime()) + .reason(actionVacation.getReason()) + .subject(actionVacation.getSubject()) + .build(); + context.post(vacationReply.getSender(), vacationReply.getRecipients(), vacationReply.getMimeMessage()); + } + + private boolean isStillInVacation(ActionVacation actionVacation, int dayDifference) { + return dayDifference >= 0 && dayDifference <= actionVacation.getDuration(); + } + + private boolean isValidForReply(final Mail mail, ActionVacation actionVacation, final ActionContext context) { + Set<MailAddress> currentMailAddresses = ImmutableSet.copyOf(mail.getRecipients()); + Set<MailAddress> allowedMailAddresses = ImmutableSet.<MailAddress>builder().addAll( + Lists.transform(actionVacation.getAddresses(), new Function<String, MailAddress>() { + public MailAddress apply(String s) { + return retrieveAddressFromString(s, context); + } + })) + .add(context.getRecipient()) + .build(); + return !Sets.intersection(currentMailAddresses, allowedMailAddresses).isEmpty(); + } + + private MailAddress retrieveAddressFromString(String address, ActionContext context) { + try { + return new MailAddress(address); + } catch (AddressException e) { + context.getLog().warn("Mail address " + address + " was not well formatted : " + e.getLocalizedMessage()); + return null; + } + } + + private boolean isMailingList(Mail mail) throws MessagingException { + Enumeration enumeration = mail.getMessage().getAllHeaderLines(); + while (enumeration.hasMoreElements()) { + String headerName = (String) enumeration.nextElement(); + if (headerName.startsWith("List-")) { + return true; + } + } + return false; + } +}
diff --git a/mailet/src/main/java/org/apache/jsieve/mailet/VacationReply.java b/mailet/src/main/java/org/apache/jsieve/mailet/VacationReply.java new file mode 100644 index 0000000..c15f5d5 --- /dev/null +++ b/mailet/src/main/java/org/apache/jsieve/mailet/VacationReply.java
@@ -0,0 +1,169 @@ +/**************************************************************** + * 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.mailet; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import org.apache.mailet.Mail; +import org.apache.mailet.MailAddress; + +import javax.activation.DataHandler; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.AddressException; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.util.ByteArrayDataSource; +import java.io.IOException; +import java.util.List; + +public class VacationReply { + + + public static class Builder { + + private final Mail originalMail; + private final ActionContext context; + private String from; + private String reason; + private String mime; + private String subject; + + public Builder(Mail originalMail, ActionContext context) { + this.originalMail = originalMail; + this.context = context; + } + + public Builder from(String from) { + this.from = from; + return this; + } + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + public Builder mime(String mime) { + this.mime = mime; + return this; + } + + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + public VacationReply build() throws MessagingException { + Preconditions.checkState(eitherReasonOrMime()); + ActionUtils.detectAndHandleLocalLooping(originalMail, context, "vacation"); + + MimeMessage reply = (MimeMessage) originalMail.getMessage().reply(false); + reply.setSubject(generateNotificationSubject()); + reply.setContent(generateNotificationContent()); + + return new VacationReply(retrieveOriginalSender(), Lists.newArrayList(originalMail.getSender()), reply); + } + + private boolean eitherReasonOrMime() { + return (reason == null) ^ (mime == null); + } + + private String generateNotificationSubject() { + return Optional.fromNullable(subject) + .or(context.getRecipient() + " is currently in vacation"); + } + + private Multipart generateNotificationContent() throws MessagingException { + try { + if (reason != null) { + return generateNotificationContentFromReasonString(); + } else { + return generateNotificationContentFromMime(); + } + } catch (IOException e) { + throw new MessagingException("Cannot read specified content", e); + } + } + + private Multipart generateNotificationContentFromMime() throws MessagingException, IOException { + return new MimeMultipart(new ByteArrayDataSource(mime, "mixed")); + } + + private Multipart generateNotificationContentFromReasonString() throws MessagingException, IOException { + Multipart multipart = new MimeMultipart("mixed"); + MimeBodyPart reasonPart = new MimeBodyPart(); + reasonPart.setDataHandler( + new DataHandler( + new ByteArrayDataSource( + reason, + "text/plain; charset=UTF-8"))); + reasonPart.setDisposition(MimeBodyPart.INLINE); + multipart.addBodyPart(reasonPart); + return multipart; + } + + private MailAddress retrieveOriginalSender() throws AddressException { + return Optional.fromNullable(from).transform(new Function<String, MailAddress>() { + public MailAddress apply(String address) { + return retrieveAddressFromString(address, context); + } + }).or(context.getRecipient()); + } + + private MailAddress retrieveAddressFromString(String address, ActionContext context) { + try { + return new MailAddress(address); + } catch (AddressException e) { + context.getLog().warn("Mail address " + address + " was not well formatted : " + e.getLocalizedMessage()); + return null; + } + } + } + + public static Builder builder(Mail originalMail, ActionContext context) { + return new Builder(originalMail, context); + } + + private final MailAddress sender; + private final List<MailAddress> recipients; + private final MimeMessage mimeMessage; + + private VacationReply(MailAddress sender, List<MailAddress> recipients, MimeMessage mimeMessage) { + this.sender = sender; + this.recipients = recipients; + this.mimeMessage = mimeMessage; + } + + public MailAddress getSender() { + return sender; + } + + public List<MailAddress> getRecipients() { + return recipients; + } + + public MimeMessage getMimeMessage() { + return mimeMessage; + } +}