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