View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  package org.apache.james.transport.mailets;
19  
20  import org.apache.avalon.framework.service.ServiceManager;
21  import org.apache.avalon.framework.configuration.Configuration;
22  import org.apache.james.Constants;
23  import org.apache.james.services.UsersRepository;
24  import org.apache.james.services.UsersStore;
25  import org.apache.mailet.RFC2822Headers;
26  import org.apache.james.util.XMLResources;
27  import org.apache.mailet.GenericMailet;
28  import org.apache.mailet.Mail;
29  import org.apache.mailet.MailAddress;
30  import org.apache.mailet.MailetException;
31  
32  import javax.mail.MessagingException;
33  import javax.mail.internet.MimeMessage;
34  import javax.mail.internet.MimeMultipart;
35  import javax.mail.internet.ParseException;
36  import java.io.IOException;
37  import java.lang.reflect.Field;
38  import java.util.ArrayList;
39  import java.util.Collection;
40  import java.util.Iterator;
41  import java.util.Properties;
42  
43  
44  /***
45   * CommandListservProcessor processes messages intended for the list serv mailing list.
46   * For command handling, see {@link CommandListservManager} <br />
47   *
48   * This class is based on the existing list serv processor shipped with James.
49   * <br />
50   * <br />
51   *
52   * To configure the CommandListservProcessor place this configuratin in the root processor:
53   * <pre>
54   * &lt;mailet match="RecipientIs=announce@localhost" class="CommandListservProcessor"&gt;
55   *  &lt;membersonly&gt;false&lt;/membersonly&gt;
56   *  &lt;attachmentsallowed&gt;true&lt;/attachmentsallowed&gt;
57   *  &lt;replytolist&gt;true&lt;/replytolist&gt;
58   *  &lt;repositoryName&gt;list-announce&lt;/repositoryName&gt;
59   *  &lt;subjectprefix&gt;Announce&lt;/subjectprefix&gt;
60   *  &lt;autobracket&gt;true&lt;/autobracket&gt;
61   *  &lt;listOwner&gt;owner@localhost&lt;/listOwner&gt;
62   *  &lt;listName&gt;announce&lt;/listName&gt;
63   * &lt;/mailet&gt;
64   *
65   * </pre>
66   *
67   * @version CVS $Revision: 382444 $ $Date: 2006-03-02 16:56:32 +0000 (gio, 02 mar 2006) $
68   * @since 2.2.0
69   */
70  public class CommandListservProcessor extends GenericMailet {
71  
72      /***
73       * Whether only members can post to the list specified by the config param: 'membersonly'.
74       * <br />
75       * eg: <pre>&lt;membersonly&gt;false&lt;/membersonly&gt;</pre>
76       *
77       * Defaults to false
78       */
79      protected boolean membersOnly;
80  
81      /***
82       * Whether attachments can be sent to the list specified by the config param: 'attachmentsallowed'.
83       * <br />
84       * eg: <pre>&lt;attachmentsallowed&gt;true&lt;/attachmentsallowed&gt;</pre>
85       *
86       * Defaults to true
87       */
88      protected boolean attachmentsAllowed;
89  
90      /***
91       * Whether the reply-to header should be set to the list address
92       * specified by the config param: 'replytolist'.
93       * <br />
94       * eg: <pre>&lt;replytolist&gt;true&lt;/replytolist&gt;</pre>
95       *
96       * Defaults to true
97       */
98      protected boolean replyToList;
99  
100     /***
101      * A String to prepend to the subject of the message when it is sent to the list
102      * specified by the config param: 'subjectPrefix'.
103      * <br />
104      * eg: <pre>&lt;subjectPrefix&gt;MyList&lt;/subjectPrefix&gt;</pre>
105      *
106      * For example: MyList
107      */
108     protected String subjectPrefix;
109 
110     /***
111      * Whether the subject prefix should be bracketed with '[' and ']'
112      * specified by the config param: 'autoBracket'.
113      * <br />
114      * eg: <pre>&lt;autoBracket&gt;true&lt;/autoBracket&gt;</pre>
115      *
116      * Defaults to true
117      */
118     protected boolean autoBracket;
119 
120     /***
121      * The repository containing the users on this list
122      * specified by the config param: 'repositoryName'.
123      * <br />
124      * eg: <pre>&lt;repositoryName&gt;list-announce&lt;/repositoryName&gt;</pre>
125      */
126     protected UsersRepository usersRepository;
127 
128     /***
129      * The list owner
130      * specified by the config param: 'listOwner'.
131      * <br />
132      * eg: <pre>&lt;listOwner&gt;owner@localhost&lt;/listOwner&gt;</pre>
133      */
134     protected MailAddress listOwner;
135 
136     /***
137      * Name of the mailing list
138      * specified by the config param: 'listName'.
139      * <br />
140      * eg: <pre>&lt;listName&gt;announce&lt;/listName&gt;</pre>
141      *
142      */
143     protected String listName;
144 
145     /***
146      * The list serv manager
147      */
148     protected ICommandListservManager commandListservManager;
149 
150     /***
151      * Mailet that will add the footer to the message
152      */
153     protected CommandListservFooter commandListservFooter;
154 
155     /***
156      * @see XMLResources
157      */
158     protected XMLResources xmlResources;
159 
160     protected boolean specificPostersOnly;
161     protected Collection allowedPosters;
162 
163     /***
164      * Initialize the mailet
165      */
166     public void init() throws MessagingException {
167         try {
168             Configuration configuration = (Configuration) getField(getMailetConfig(), "configuration");
169 
170             membersOnly = getBoolean("membersonly", false);
171             attachmentsAllowed = getBoolean("attachmentsallowed", true);
172             replyToList = getBoolean("replytolist", true);
173             subjectPrefix = getString("subjectprefix", null);
174             listName = getString("listName", null);
175             autoBracket = getBoolean("autobracket", true);
176             listOwner = new MailAddress(getString("listOwner", null));
177             specificPostersOnly = getBoolean("specifiedpostersonly", false);
178             //initialize resources
179             initializeResources();
180             //init user repos
181             initUsersRepository();
182             initAllowedPosters(configuration);
183         } catch (Exception e) {
184             throw new MessagingException(e.getMessage(), e);
185         }
186     }
187 
188     /***
189      * A message was sent to the list serv.  Broadcast if appropriate...
190      * @param mail
191      * @throws MessagingException
192      */
193     public void service(Mail mail) throws MessagingException {
194         try {
195             Collection members = getMembers();
196             MailAddress listservAddr = (MailAddress) mail.getRecipients().iterator().next();
197 
198             // Check if allowed to post
199             if (!checkAllowedPoster(mail, members)) {
200                 return;
201             }
202 
203             //Check for no attachments
204             if (!checkAnnouncements(mail)) {
205                 return;
206             }
207 
208             //check been there
209             if (!checkBeenThere(listservAddr, mail)) {
210                 return;
211             }
212 
213             //addfooter
214             addFooter(mail);
215 
216             //prepare the new message
217             MimeMessage message = prepareListMessage(mail, listservAddr);
218 
219             //Set the subject if set
220             setSubject(message);
221 
222             //Send the message to the list members
223             //We set the list owner as the sender for now so bounces go to him/her
224             getMailetContext().sendMail(listOwner, members, message);
225         } catch (IOException ioe) {
226             throw new MailetException("Error creating listserv message", ioe);
227         } finally {
228             //Kill the old message
229             mail.setState(Mail.GHOST);
230         }
231     }
232 
233     /***
234      * Add the footer using {@link CommandListservFooter}
235      * @param mail
236      * @throws MessagingException
237      */
238     protected void addFooter(Mail mail) throws MessagingException {
239         getCommandListservFooter().service(mail);
240     }
241 
242     protected void setSubject(MimeMessage message) throws MessagingException {
243         String prefix = subjectPrefix;
244         if (prefix != null) {
245             if (autoBracket) {
246                 StringBuffer prefixBuffer =
247                         new StringBuffer(64)
248                         .append("[")
249                         .append(prefix)
250                         .append("]");
251                 prefix = prefixBuffer.toString();
252             }
253             String subj = message.getSubject();
254             if (subj == null) {
255                 subj = "";
256             }
257             subj = normalizeSubject(subj, prefix);
258             AbstractRedirect.changeSubject(message, subj);
259         }
260     }
261 
262     /***
263      * Create a new message with some set headers
264      * @param mail
265      * @param listservAddr
266      * @return a prepared List Message
267      * @throws MessagingException
268      */
269     protected MimeMessage prepareListMessage(Mail mail, MailAddress listservAddr) throws MessagingException {
270         //Create a copy of this message to send out
271         MimeMessage message = new MimeMessage(mail.getMessage());
272 
273         //We need tao remove this header from the copy we're sending around
274         message.removeHeader(RFC2822Headers.RETURN_PATH);
275 
276         //We're going to set this special header to avoid bounces
277         //  getting sent back out to the list
278         message.setHeader("X-been-there", listservAddr.toString());
279 
280         //If replies should go to this list, we need to set the header
281         if (replyToList) {
282             message.setHeader(RFC2822Headers.REPLY_TO, listservAddr.toString());
283         }
284 
285         return message;
286     }
287 
288     /***
289      * return true if this is ok, false otherwise
290      * Check if the X-been-there header is set to the listserv's name
291      * (the address).  If it has, this means it's a message from this
292      * listserv that's getting bounced back, so we need to swallow it
293      *
294      * @param listservAddr
295      * @param mail
296      * @return true if this message has already bounced, false otherwse
297      * @throws MessagingException
298      */
299     protected boolean checkBeenThere(MailAddress listservAddr, Mail mail) throws MessagingException {
300         if (listservAddr.equals(mail.getMessage().getHeader("X-been-there"))) {
301             return false;
302         }
303         return true;
304     }
305 
306     /***
307      * Returns true if this is ok to send to the list
308      * @param mail
309      * @return true if this message is ok, false otherwise
310      * @throws IOException
311      * @throws MessagingException
312      */
313     protected boolean checkAnnouncements(Mail mail) throws IOException, MessagingException {
314         if (!attachmentsAllowed && mail.getMessage().getContent() instanceof MimeMultipart) {
315             Properties standardProperties = getCommandListservManager().getStandardProperties();
316 
317             getCommandListservManager().onError(mail,
318                     xmlResources.getString("invalid.mail.subject", standardProperties),
319                     xmlResources.getString("error.attachments", standardProperties));
320             return false;
321         }
322         return true;
323     }
324 
325     /***
326      * Returns true if this user is ok to send to the list
327      *
328      * @param members
329      * @param mail
330      * @return true if this message is ok, false otherwise
331      * @throws MessagingException
332      */
333     protected boolean checkMembers(Collection members, Mail mail) throws MessagingException {
334         if (membersOnly && !members.contains(mail.getSender())) {
335             Properties standardProperties = getCommandListservManager().getStandardProperties();
336             getCommandListservManager().onError(mail,
337                     xmlResources.getString("invalid.mail.subject", standardProperties),
338                     xmlResources.getString("error.membersonly", standardProperties));
339 
340             return false;
341         }
342         return true;
343     }
344 
345     public Collection getMembers() throws ParseException {
346         Collection reply = new ArrayList();
347         for (Iterator it = usersRepository.list(); it.hasNext();) {
348             String member = it.next().toString();
349             try {
350                 reply.add(new MailAddress(member));
351             } catch (Exception e) {
352                 // Handle an invalid subscriber address by logging it and
353                 // proceeding to the next member.
354                 StringBuffer logBuffer =
355                         new StringBuffer(1024)
356                         .append("Invalid subscriber address: ")
357                         .append(member)
358                         .append(" caused: ")
359                         .append(e.getMessage());
360                 log(logBuffer.toString());
361             }
362         }
363         return reply;
364     }
365 
366     /***
367      * Get a configuration value
368      * @param attrName
369      * @param defValue
370      * @return the value if found, defValue otherwise
371      */
372     protected boolean getBoolean(String attrName, boolean defValue) {
373         boolean value = defValue;
374         try {
375             value = new Boolean(getInitParameter(attrName)).booleanValue();
376         } catch (Exception e) {
377             // Ignore any exceptions, default to false
378         }
379         return value;
380     }
381 
382     /***
383      * Get a configuration value
384      * @param attrName
385      * @param defValue
386      * @return the attrValue if found, defValue otherwise
387      */
388     protected String getString(String attrName, String defValue) {
389         String value = defValue;
390         try {
391             value = getInitParameter(attrName);
392         } catch (Exception e) {
393             // Ignore any exceptions, default to false
394         }
395         return value;
396     }
397 
398     /***
399      * initialize the resources
400      * @throws Exception
401      */
402     protected void initializeResources() throws Exception {
403         xmlResources = getCommandListservManager().initXMLResources(new String[]{"List Manager"})[0];
404     }
405 
406     /***
407      * Fetch the repository of users
408      */
409     protected void initUsersRepository() throws Exception {
410         ServiceManager compMgr = (ServiceManager) getMailetContext().getAttribute(Constants.AVALON_COMPONENT_MANAGER);
411         UsersStore usersStore = (UsersStore) compMgr.lookup(UsersStore.ROLE);
412         String repName = getInitParameter("repositoryName");
413 
414         usersRepository = usersStore.getRepository(repName);
415         if (usersRepository == null) throw new Exception("Invalid user repository: " + repName);
416     }
417 
418     /***
419      * <p>This takes the subject string and reduces (normailzes) it.
420      * Multiple "Re:" entries are reduced to one, and capitalized.  The
421      * prefix is always moved/placed at the beginning of the line, and
422      * extra blanks are reduced, so that the output is always of the
423      * form:</p>
424      * <code>
425      * &lt;prefix&gt; + &lt;one-optional-"Re:"*gt; + &lt;remaining subject&gt;
426      * </code>
427      * <p>I have done extensive testing of this routine with a standalone
428      * driver, and am leaving the commented out debug messages so that
429      * when someone decides to enhance this method, it can be yanked it
430      * from this file, embedded it with a test driver, and the comments
431      * enabled.</p>
432      */
433     static private String normalizeSubject(final String subj, final String prefix) {
434         // JDK IMPLEMENTATION NOTE!  When we require JDK 1.4+, all
435         // occurrences of subject.toString.().indexOf(...) can be
436         // replaced by subject.indexOf(...).
437 
438         StringBuffer subject = new StringBuffer(subj);
439         int prefixLength = prefix.length();
440 
441         // System.err.println("In:  " + subject);
442 
443         // If the "prefix" is not at the beginning the subject line, remove it
444         int index = subject.toString().indexOf(prefix);
445         if (index != 0) {
446             // System.err.println("(p) index: " + index + ", subject: " + subject);
447             if (index > 0) {
448                 subject.delete(index, index + prefixLength);
449             }
450             subject.insert(0, prefix); // insert prefix at the front
451         }
452 
453         // Replace Re: with RE:
454         String match = "Re:";
455         index = subject.toString().indexOf(match, prefixLength);
456 
457         while(index > -1) {
458             // System.err.println("(a) index: " + index + ", subject: " + subject);
459             subject.replace(index, index + match.length(), "RE:");
460             index = subject.toString().indexOf(match, prefixLength);
461             // System.err.println("(b) index: " + index + ", subject: " + subject);
462         }
463 
464         // Reduce them to one at the beginning
465         match ="RE:";
466         int indexRE = subject.toString().indexOf(match, prefixLength) + match.length();
467         index = subject.toString().indexOf(match, indexRE);
468         while(index > 0) {
469             // System.err.println("(c) index: " + index + ", subject: " + subject);
470             subject.delete(index, index + match.length());
471             index = subject.toString().indexOf(match, indexRE);
472             // System.err.println("(d) index: " + index + ", subject: " + subject);
473         }
474 
475         // Reduce blanks
476         match = "  ";
477         index = subject.toString().indexOf(match, prefixLength);
478         while(index > -1) {
479             // System.err.println("(e) index: " + index + ", subject: " + subject);
480             subject.replace(index, index + match.length(), " ");
481             index = subject.toString().indexOf(match, prefixLength);
482             // System.err.println("(f) index: " + index + ", subject: " + subject);
483         }
484 
485 
486         // System.err.println("Out: " + subject);
487 
488         return subject.toString();
489     }
490 
491     /***
492      * lazy retrieval
493      * @return ICommandListservManager
494      */
495     protected ICommandListservManager getCommandListservManager() {
496         if (commandListservManager == null) {
497             commandListservManager = (ICommandListservManager) getMailetContext().getAttribute(ICommandListservManager.ID + listName);
498             if (commandListservManager == null) {
499                 throw new IllegalStateException("Unable to find command list manager named: " + listName);
500             }
501         }
502 
503         return commandListservManager;
504     }
505 
506     /***
507      * Lazy init
508      * @throws MessagingException
509      */
510     protected CommandListservFooter getCommandListservFooter() throws MessagingException {
511         if (commandListservFooter == null) {
512             commandListservFooter = new CommandListservFooter(getCommandListservManager());
513             commandListservFooter.init(getMailetConfig());
514         }
515         return commandListservFooter;
516     }
517 
518     /***
519      * Retrieves a data field, potentially defined by a super class.
520      * @return null if not found, the object otherwise
521      */
522     protected static Object getField(Object instance, String name) throws IllegalAccessException {
523         Class clazz = instance.getClass();
524         Field[] fields;
525         while (clazz != null) {
526             fields = clazz.getDeclaredFields();
527             for (int index = 0; index < fields.length; index++) {
528                 Field field = fields[index];
529                 if (field.getName().equals(name)) {
530                     field.setAccessible(true);
531                     return field.get(instance);
532                 }
533             }
534             clazz = clazz.getSuperclass();
535         }
536 
537         return null;
538     }
539 
540     protected void initAllowedPosters(Configuration configuration) throws Exception {
541         final Configuration allowedPostersElement = configuration.getChild("allowedposters");
542         allowedPosters = new ArrayList();
543         if (allowedPostersElement != null) {
544             final Configuration[] addresses = allowedPostersElement.getChildren("address");
545             for (int index = 0; index < addresses.length; index++) {
546                 Configuration address = addresses[index];
547                 String emailAddress = address.getValue();
548                 allowedPosters.add(new MailAddress(emailAddress));
549             }
550         }
551     }
552 
553     /***
554      * Returns true if this user is ok to send to the list
555      *
556      * @param mail
557      * @return true if this message is ok, false otherwise
558      * @throws MessagingException
559      */
560     protected boolean checkAllowedPoster(Mail mail, Collection members) throws MessagingException {
561         /*
562         if we don't require someone to be an allowed poster, then allow post if we don't require require them to be a subscriber, or they are one.
563         if the sender is in the allowed list, post
564         */
565         if ((!specificPostersOnly && (!membersOnly || members.contains(mail.getSender()))) || allowedPosters.contains(mail.getSender())) {
566             return true;
567         } else {
568             Properties standardProperties = getCommandListservManager().getStandardProperties();
569             getCommandListservManager().onError(mail,
570                                                 xmlResources.getString("invalid.mail.subject", standardProperties),
571                                                 xmlResources.getString("error.membersonly", standardProperties));
572             return false;
573         }
574     }
575 }