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