| /**************************************************************** |
| * 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 java.io.InputStream; |
| import java.util.Collection; |
| import java.util.Enumeration; |
| import java.util.Iterator; |
| import java.util.Vector; |
| |
| import javax.mail.Header; |
| import javax.mail.MessagingException; |
| import javax.mail.internet.InternetHeaders; |
| import javax.mail.internet.MimeMessage; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.jsieve.ConfigurationManager; |
| import org.apache.jsieve.SieveConfigurationException; |
| import org.apache.jsieve.SieveFactory; |
| import org.apache.mailet.Mail; |
| import org.apache.mailet.MailAddress; |
| import org.apache.mailet.MailetConfig; |
| import org.apache.mailet.MailetException; |
| import org.apache.mailet.base.GenericMailet; |
| import org.apache.mailet.base.RFC2822Headers; |
| |
| /** |
| * <p>Executes a <a href='http://www.rfc-editor.org/rfc/rfc3028.txt'>Sieve</a> |
| * script against incoming mail. The script applied is based on the recipient.</p> |
| * <h4>Init Parameters</h4> |
| * <table> |
| * <thead><tr><th>Name</th><th>Required</th><th>Values</th><th>Role</th></thead> |
| * <tr><td>verbose</td><td>No - defaults to false</td><td>true (ignoring case) to enable, otherwise disable</td> |
| * <td> |
| * Enables verbose logging. |
| * </td></tr> |
| * </table> |
| */ |
| public class SieveMailboxMailet extends GenericMailet { |
| |
| /** |
| * The delivery header |
| */ |
| private String deliveryHeader; |
| |
| /** |
| * resetReturnPath |
| */ |
| private boolean resetReturnPath; |
| /** Experimental */ |
| private Poster poster; |
| /** Experimental */ |
| private ResourceLocator locator; |
| |
| /** Indicates whether this mailet should log verbosely */ |
| private boolean verbose = false; |
| |
| private boolean consume = true; |
| /** Indicates whether this mailet should log minimal information */ |
| private boolean quiet = true; |
| |
| private SieveFactory factory; |
| |
| private ActionDispatcher actionDispatcher; |
| |
| private Log log; |
| |
| /** |
| * For SDI |
| */ |
| public SieveMailboxMailet() {} |
| |
| /** |
| * CDI |
| * @param poster not null |
| */ |
| public SieveMailboxMailet(Poster poster, ResourceLocator locator) { |
| this(); |
| this.poster = poster; |
| this.locator = locator; |
| } |
| |
| |
| public ResourceLocator getLocator() { |
| return locator; |
| } |
| |
| /** |
| * For SDI |
| * @param locator not null |
| */ |
| public void setLocator(ResourceLocator locator) { |
| this.locator = locator; |
| } |
| |
| public Poster getPoster() { |
| return poster; |
| } |
| |
| /** |
| * For SDI |
| * @param poster not null |
| */ |
| public void setPoster(Poster poster) { |
| this.poster = poster; |
| } |
| |
| /** |
| * Is this mailet GHOSTing all mail it processes? |
| * @return true when mailet consumes all mail, false otherwise |
| */ |
| public boolean isConsume() { |
| return consume; |
| } |
| |
| /** |
| * Sets whether this mailet should GHOST all mail. |
| * @param consume true when the mailet should consume all mail, |
| * false otherwise |
| */ |
| public void setConsume(boolean consume) { |
| this.consume = consume; |
| } |
| |
| /** |
| * Is this mailet logging verbosely? |
| * This property is set by init parameters. |
| * @return true if logging should be verbose, false otherwise |
| */ |
| public boolean isVerbose() { |
| return verbose; |
| } |
| |
| |
| /** |
| * Sets whether logging should be verbose for this mailet. |
| * This property is set by init parameters. |
| * This setting overrides {@link #isQuiet()}. |
| * @param verbose true when logging should be verbose, |
| * false otherwise |
| */ |
| public void setVerbose(boolean verbose) { |
| this.verbose = verbose; |
| } |
| |
| /** |
| * Is the logging for this mailet set to minimal? |
| * @return true |
| */ |
| public boolean isQuiet() { |
| return quiet; |
| } |
| |
| /** |
| * Sets the logging for this mailet to minimal. |
| * This is overriden by {@link #setVerbose(boolean)}. |
| * @param quiet true for minimal logging, false otherwise |
| */ |
| public void setQuiet(boolean quiet) { |
| this.quiet = quiet; |
| } |
| |
| |
| /** |
| * Is informational logging turned on? |
| * @return true when minimal logging is off, |
| * false when logging is minimal |
| */ |
| public boolean isInfoLoggingOn() { |
| return verbose || !quiet; |
| } |
| |
| @Override |
| public void init(MailetConfig config) throws MessagingException { |
| |
| super.init(config); |
| |
| try { |
| final ConfigurationManager configurationManager = new ConfigurationManager(); |
| final int logLevel; |
| if (verbose) { |
| logLevel = CommonsLoggingAdapter.TRACE; |
| } else if (quiet) { |
| logLevel = CommonsLoggingAdapter.FATAL; |
| } else { |
| logLevel = CommonsLoggingAdapter.WARN; |
| } |
| log = new CommonsLoggingAdapter(this, logLevel); |
| configurationManager.setLog(log); |
| factory = configurationManager.build(); |
| } catch (SieveConfigurationException e) { |
| throw new MessagingException("Failed to load standard Sieve configuration.", e); |
| } |
| } |
| |
| /** |
| * Delivers a mail to a local mailbox. |
| * |
| * @param mail |
| * the mail being processed |
| * |
| * @throws MessagingException |
| * if an error occurs while storing the mail |
| */ |
| @SuppressWarnings("unchecked") |
| @Override |
| public void service(Mail mail) throws MessagingException { |
| Collection<MailAddress> recipients = mail.getRecipients(); |
| Collection<MailAddress> errors = new Vector<MailAddress>(); |
| |
| MimeMessage message = null; |
| if (deliveryHeader != null || resetReturnPath) { |
| message = mail.getMessage(); |
| } |
| |
| if (resetReturnPath) { |
| // Set Return-Path and remove all other Return-Path headers from the |
| // message |
| // This only works because there is a placeholder inserted by |
| // MimeMessageWrapper |
| message.setHeader(RFC2822Headers.RETURN_PATH, |
| (mail.getSender() == null ? "<>" : "<" + mail.getSender() |
| + ">")); |
| } |
| |
| Enumeration headers; |
| InternetHeaders deliveredTo = new InternetHeaders(); |
| if (deliveryHeader != null) { |
| // Copy any Delivered-To headers from the message |
| headers = message |
| .getMatchingHeaders(new String[] { deliveryHeader }); |
| while (headers.hasMoreElements()) { |
| Header header = (Header) headers.nextElement(); |
| deliveredTo.addHeader(header.getName(), header.getValue()); |
| } |
| } |
| |
| for (Iterator<MailAddress> i = recipients.iterator(); i.hasNext();) { |
| MailAddress recipient = i.next(); |
| try { |
| if (deliveryHeader != null) { |
| // Add qmail's de facto standard Delivered-To header |
| message.addHeader(deliveryHeader, recipient.toString()); |
| } |
| |
| storeMail(mail.getSender(), recipient, mail); |
| |
| if (deliveryHeader != null) { |
| if (i.hasNext()) { |
| // Remove headers but leave all placeholders |
| message.removeHeader(deliveryHeader); |
| headers = deliveredTo.getAllHeaders(); |
| // And restore any original Delivered-To headers |
| while (headers.hasMoreElements()) { |
| Header header = (Header) headers.nextElement(); |
| message.addHeader(header.getName(), header |
| .getValue()); |
| } |
| } |
| } |
| } catch (Exception ex) { |
| log("Error while storing mail.", ex); |
| errors.add(recipient); |
| } |
| } |
| |
| if (!errors.isEmpty()) { |
| // If there were errors, we redirect the email to the ERROR |
| // processor. |
| // In order for this server to meet the requirements of the SMTP |
| // specification, mails on the ERROR processor must be returned to |
| // the sender. Note that this email doesn't include any details |
| // regarding the details of the failure(s). |
| // In the future we may wish to address this. |
| getMailetContext().sendMail(mail.getSender(), errors, |
| mail.getMessage(), Mail.ERROR); |
| } |
| if (consume) { |
| // Consume this message |
| mail.setState(Mail.GHOST); |
| } |
| } |
| |
| /** |
| * Return a string describing this mailet. |
| * |
| * @return a string describing this mailet |
| */ |
| @Override |
| public String getMailetInfo() { |
| return "Sieve Mailbox Mailet"; |
| } |
| |
| /** |
| * |
| * @param sender |
| * @param recipient |
| * @param mail |
| * @throws MessagingException |
| */ |
| @SuppressWarnings("deprecation") |
| public void storeMail(MailAddress sender, MailAddress recipient, |
| Mail mail) throws MessagingException { |
| if (recipient == null) { |
| throw new IllegalArgumentException( |
| "Recipient for mail to be spooled cannot be null."); |
| } |
| if (mail.getMessage() == null) { |
| throw new IllegalArgumentException( |
| "Mail message to be spooled cannot be null."); |
| } |
| |
| sieveMessage(recipient, mail); |
| |
| } |
| |
| void sieveMessage(MailAddress recpient, Mail aMail) throws MessagingException { |
| String username = getUsername(recpient); |
| // Evaluate the script against the mail |
| String relativeUri = "//" + username +"/sieve"; |
| try |
| { |
| final InputStream ins = locator.get(relativeUri); |
| |
| SieveMailAdapter aMailAdapter = new SieveMailAdapter(aMail, |
| getMailetContext(), actionDispatcher, poster); |
| aMailAdapter.setLog(log); |
| // This logging operation is potentially costly |
| if (verbose) { |
| log("Evaluating " + aMailAdapter.toString() + "against \"" |
| + relativeUri + "\""); |
| } |
| factory.evaluate(aMailAdapter, factory.parse(ins)); |
| } |
| catch (Exception ex) |
| { |
| // |
| // SLIEVE is a mail filtering protocol. |
| // Rejecting the mail because it cannot be filtered |
| // seems very unfriendly. |
| // So just log and store in INBOX. |
| // |
| if (isInfoLoggingOn()) { |
| log("Cannot evaluate Sieve script. Storing mail in user INBOX.", ex); |
| } |
| storeMessageInbox(username, aMail); |
| } |
| } |
| |
| void storeMessageInbox(String username, Mail mail) throws MessagingException { |
| String url = "mailbox://" + username + "/"; |
| poster.post(url, mail.getMessage()); |
| } |
| |
| /** |
| * @see org.apache.mailet.base.GenericMailet#init() |
| */ |
| @Override |
| public void init() throws MessagingException { |
| super.init(); |
| if (poster == null || locator == null) { |
| throw new MailetException("Not initialised. Please ensure that the mailet container supports either" + |
| " setter or constructor injection"); |
| } |
| |
| this.deliveryHeader = getInitParameter("addDeliveryHeader"); |
| this.resetReturnPath = getInitParameter("resetReturnPath", true); |
| this.consume = getInitParameter("consume", true); |
| this.verbose = getInitParameter("verbose", false); |
| this.quiet = getInitParameter("quiet", false); |
| |
| actionDispatcher = new ActionDispatcher(); |
| } |
| |
| /** |
| * Return the username to use for sieve processing for the given MailAddress |
| * |
| * @param m |
| * @return username |
| */ |
| protected String getUsername(MailAddress m) { |
| return m.getLocalPart() + "@localhost"; |
| } |
| } |