View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.jsieve.mailet;
21  
22  import java.io.InputStream;
23  import java.util.Collection;
24  import java.util.Enumeration;
25  import java.util.Iterator;
26  import java.util.Vector;
27  
28  import javax.mail.Header;
29  import javax.mail.MessagingException;
30  import javax.mail.internet.InternetHeaders;
31  import javax.mail.internet.MimeMessage;
32  
33  import org.apache.commons.logging.Log;
34  import org.apache.jsieve.ConfigurationManager;
35  import org.apache.jsieve.SieveConfigurationException;
36  import org.apache.jsieve.SieveFactory;
37  import org.apache.mailet.Mail;
38  import org.apache.mailet.MailAddress;
39  import org.apache.mailet.MailetConfig;
40  import org.apache.mailet.MailetException;
41  import org.apache.mailet.base.GenericMailet;
42  import org.apache.mailet.base.RFC2822Headers;
43  
44  /**
45   * <p>Executes a <a href='http://www.rfc-editor.org/rfc/rfc3028.txt'>Sieve</a>
46   * script against incoming mail. The script applied is based on the recipient.</p>
47   * <h4>Init Parameters</h4>
48   * <table>
49   * <thead><tr><th>Name</th><th>Required</th><th>Values</th><th>Role</th></thead>
50   * <tr><td>verbose</td><td>No - defaults to false</td><td>true (ignoring case) to enable, otherwise disable</td>
51   * <td>
52   * Enables verbose logging.
53   * </td></tr>
54   * </table>
55   */
56  public class SieveMailboxMailet extends GenericMailet {
57  
58      /**
59       * The delivery header
60       */
61      private String deliveryHeader;
62  
63      /**
64       * resetReturnPath
65       */
66      private boolean resetReturnPath;
67      /** Experimental */
68      private Poster poster;
69      /** Experimental */
70      private ResourceLocator locator;
71      
72      /** Indicates whether this mailet should log verbosely */
73      private boolean verbose = false;
74      
75      private boolean consume = true;
76      /** Indicates whether this mailet should log minimal information */
77      private boolean quiet = true;
78  
79      private SieveFactory factory;
80  
81      private ActionDispatcher actionDispatcher;
82  
83      private Log log;
84  
85      /**
86       * For SDI
87       */
88      public SieveMailboxMailet() {}
89      
90      /**
91       * CDI
92       * @param poster not null
93       */
94      public SieveMailboxMailet(Poster poster, ResourceLocator locator) {
95          this();
96          this.poster = poster;
97          this.locator = locator;
98      }
99  
100     
101     public ResourceLocator getLocator() {
102         return locator;
103     }
104 
105     /**
106      * For SDI
107      * @param locator not null
108      */
109     public void setLocator(ResourceLocator locator) {
110         this.locator = locator;
111     }
112 
113     public Poster getPoster() {
114         return poster;
115     }
116     
117     /**
118      * For SDI
119      * @param poster not null
120      */
121     public void setPoster(Poster poster) {
122         this.poster = poster;
123     }
124 
125     /**
126      * Is this mailet GHOSTing all mail it processes?
127      * @return true when mailet consumes all mail, false otherwise
128      */
129     public boolean isConsume() {
130         return consume;
131     }
132 
133     /**
134      * Sets whether this mailet should GHOST all mail.
135      * @param consume true when the mailet should consume all mail, 
136      * false otherwise
137      */
138     public void setConsume(boolean consume) {
139         this.consume = consume;
140     }
141 
142     /**
143      * Is this mailet logging verbosely?
144      * This property is set by init parameters.
145      * @return true if logging should be verbose, false otherwise
146      */
147     public boolean isVerbose() {
148         return verbose;
149     }
150 
151 
152     /**
153      * Sets whether logging should be verbose for this mailet.
154      * This property is set by init parameters.
155      * This setting overrides {@link #isQuiet()}.
156      * @param verbose true when logging should be verbose,
157      * false otherwise
158      */
159     public void setVerbose(boolean verbose) {
160         this.verbose = verbose;
161     }
162 
163     /**
164      * Is the logging for this mailet set to minimal?
165      * @return true
166      */
167     public boolean isQuiet() {
168         return quiet;
169     }
170 
171     /**
172      * Sets the logging for this mailet to minimal.
173      * This is overriden by {@link #setVerbose(boolean)}.
174      * @param quiet true for minimal logging, false otherwise
175      */
176     public void setQuiet(boolean quiet) {
177         this.quiet = quiet;
178     }
179     
180     /**
181      * Is informational logging turned on? 
182      * @return true when minimal logging is off,
183      * false when logging is minimal
184      */
185     public boolean isInfoLoggingOn() {
186         return verbose || !quiet;
187     }
188 
189     @Override
190     public void init(MailetConfig config) throws MessagingException {
191         
192         super.init(config);
193 
194         try {
195             final ConfigurationManager configurationManager = new ConfigurationManager();
196             final int logLevel;
197             if (verbose) {
198                 logLevel = CommonsLoggingAdapter.TRACE;
199             } else if (quiet) {
200                 logLevel = CommonsLoggingAdapter.FATAL;
201             } else {
202                 logLevel = CommonsLoggingAdapter.WARN;
203             }
204             log = new CommonsLoggingAdapter(this, logLevel);
205             configurationManager.setLog(log);
206             factory = configurationManager.build();
207         } catch (SieveConfigurationException e) {
208             throw new MessagingException("Failed to load standard Sieve configuration.", e);
209         }
210     }
211 
212     /**
213      * Delivers a mail to a local mailbox.
214      * 
215      * @param mail
216      *            the mail being processed
217      * 
218      * @throws MessagingException
219      *             if an error occurs while storing the mail
220      */
221     @SuppressWarnings("unchecked")
222     @Override
223     public void service(Mail mail) throws MessagingException {
224         Collection<MailAddress> recipients = mail.getRecipients();
225         Collection<MailAddress> errors = new Vector<MailAddress>();
226 
227         MimeMessage message = null;
228         if (deliveryHeader != null || resetReturnPath) {
229             message = mail.getMessage();
230         }
231 
232         if (resetReturnPath) {
233             // Set Return-Path and remove all other Return-Path headers from the
234             // message
235             // This only works because there is a placeholder inserted by
236             // MimeMessageWrapper
237             message.setHeader(RFC2822Headers.RETURN_PATH,
238                     (mail.getSender() == null ? "<>" : "<" + mail.getSender()
239                             + ">"));
240         }
241 
242         Enumeration headers;
243         InternetHeaders deliveredTo = new InternetHeaders();
244         if (deliveryHeader != null) {
245             // Copy any Delivered-To headers from the message
246             headers = message
247                     .getMatchingHeaders(new String[] { deliveryHeader });
248             while (headers.hasMoreElements()) {
249                 Header header = (Header) headers.nextElement();
250                 deliveredTo.addHeader(header.getName(), header.getValue());
251             }
252         }
253 
254         for (Iterator<MailAddress> i = recipients.iterator(); i.hasNext();) {
255             MailAddress recipient = i.next();
256             try {
257                 if (deliveryHeader != null) {
258                     // Add qmail's de facto standard Delivered-To header
259                     message.addHeader(deliveryHeader, recipient.toString());
260                 }
261 
262                 storeMail(mail.getSender(), recipient, mail);
263 
264                 if (deliveryHeader != null) {
265                     if (i.hasNext()) {
266                         // Remove headers but leave all placeholders
267                         message.removeHeader(deliveryHeader);
268                         headers = deliveredTo.getAllHeaders();
269                         // And restore any original Delivered-To headers
270                         while (headers.hasMoreElements()) {
271                             Header header = (Header) headers.nextElement();
272                             message.addHeader(header.getName(), header
273                                     .getValue());
274                         }
275                     }
276                 }
277             } catch (Exception ex) {
278                 log("Error while storing mail.", ex);
279                 errors.add(recipient);
280             }
281         }
282 
283         if (!errors.isEmpty()) {
284             // If there were errors, we redirect the email to the ERROR
285             // processor.
286             // In order for this server to meet the requirements of the SMTP
287             // specification, mails on the ERROR processor must be returned to
288             // the sender. Note that this email doesn't include any details
289             // regarding the details of the failure(s).
290             // In the future we may wish to address this.
291             getMailetContext().sendMail(mail.getSender(), errors,
292                     mail.getMessage(), Mail.ERROR);
293         }
294         if (consume) {
295             // Consume this message
296             mail.setState(Mail.GHOST);
297         }
298     }
299 
300     /**
301      * Return a string describing this mailet.
302      * 
303      * @return a string describing this mailet
304      */
305     @Override
306     public String getMailetInfo() {
307         return "Sieve Mailbox Mailet";
308     }
309 
310     /**
311      * 
312      * @param sender
313      * @param recipient
314      * @param mail
315      * @throws MessagingException
316      */
317     @SuppressWarnings("deprecation")
318     public void storeMail(MailAddress sender, MailAddress recipient,
319             Mail mail) throws MessagingException {
320         String username;
321         if (recipient == null) {
322             throw new IllegalArgumentException(
323                     "Recipient for mail to be spooled cannot be null.");
324         }
325         if (mail.getMessage() == null) {
326             throw new IllegalArgumentException(
327                     "Mail message to be spooled cannot be null.");
328         }
329         // recipient.toString was used here (JD)
330         username = recipient.getUser();
331         
332         sieveMessage(username, mail);
333  
334     }
335     
336     void sieveMessage(String username, Mail aMail) throws MessagingException {
337         // Evaluate the script against the mail
338         String relativeUri = "//" + username + "@" + "localhost/sieve"; 
339         try
340         {
341             final InputStream ins = locator.get(relativeUri);
342             
343             SieveMailAdapter aMailAdapter = new SieveMailAdapter(aMail,
344                     getMailetContext(), actionDispatcher, poster);
345             aMailAdapter.setLog(log);
346             // This logging operation is potentially costly
347             if (verbose) {
348                 log("Evaluating " + aMailAdapter.toString() + "against \""
349                     + relativeUri + "\"");
350             }
351             factory.evaluate(aMailAdapter, factory.parse(ins));
352         }
353         catch (Exception ex)
354         {
355             //
356             // SLIEVE is a mail filtering protocol.
357             // Rejecting the mail because it cannot be filtered
358             // seems very unfriendly.
359             // So just log and store in INBOX.
360             //
361             if (isInfoLoggingOn()) {
362                 log("Cannot evaluate Sieve script. Storing mail in user INBOX.", ex);
363             }
364             storeMessageInbox(username, aMail);
365         }
366     }
367     
368     void storeMessageInbox(String username, Mail mail) throws MessagingException {
369         String url = "mailbox://" + username + "@localhost/";
370         poster.post(url, mail.getMessage());
371     }
372 
373     /**
374      * @see org.apache.mailet.base.GenericMailet#init()
375      */
376     @Override
377     public void init() throws MessagingException {
378         super.init();
379         if (poster == null || locator == null) {
380             throw new MailetException("Not initialised. Please ensure that the mailet container supports either" +
381                     " setter or constructor injection");
382         }
383         
384         this.deliveryHeader = getInitParameter("addDeliveryHeader");
385         this.resetReturnPath = getInitParameter("resetReturnPath", true);
386         this.consume = getInitParameter("consume", true);
387         this.verbose = getInitParameter("verbose", false);
388         this.quiet = getInitParameter("quiet", false);
389         
390         actionDispatcher = new ActionDispatcher();
391     }
392 }