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.avalon.framework.configuration.ConfigurationException;
27  import org.apache.james.Constants;
28  import org.apache.james.services.UsersRepository;
29  import org.apache.james.services.UsersStore;
30  import org.apache.james.transport.mailets.listservcommands.ErrorCommand;
31  import org.apache.james.transport.mailets.listservcommands.IListServCommand;
32  import org.apache.james.util.XMLResources;
33  import org.apache.mailet.GenericMailet;
34  import org.apache.mailet.Mail;
35  import org.apache.mailet.MailAddress;
36  
37  import javax.mail.MessagingException;
38  import java.io.File;
39  import java.lang.reflect.Field;
40  import java.util.ArrayList;
41  import java.util.HashMap;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.Properties;
47  
48  /***
49   * CommandListservManager is the default implementation of {@link ICommandListservManager}.
50   * It loads all the configured {@link IListServCommand}s and delegates to them at runtime.
51   * <br />
52   *
53   * It isn't responsible for procesing messages sent to the main mailing list, but is responsible for
54   * individual commands sent by users, such as: info, subscribe, etc...
55   * <br />
56   *
57   * Requests sent to the CommandListservManager take the form of:
58   * <pre>
59   * &lt;listName&gt;-&lt;commandName&gt;@domain
60   * </pre>
61   *
62   * If the command isn't recognized an error will be sent using {@link #onError}.
63   * <br />
64   * <br />
65   *
66   * The configuration for this mailet sould be in the 'root' processor block.
67   * <pre>
68   * &lt;mailet match="CommandListservMatcher=announce@localhost" class="CommandListservManager"&gt;
69   *  &lt;listName&gt;announce&lt;/listName&gt;
70   *  &lt;displayName&gt;Announce mailing list&lt;/displayName&gt;
71   *  &lt;listOwner&gt;owner@localhost&lt;/listOwner&gt;
72   *  &lt;repositoryName&gt;list-announce&lt;/repositoryName&gt;
73   *  &lt;listDomain&gt;localhost&lt;/listDomain&gt;
74   *
75   *  &lt;commandpackages&gt;
76   *     &lt;commandpackage&gt;org.apache.james.transport.mailets.listservcommands&lt;/commandpackage&gt;
77   *  &lt;/commandpackages&gt;
78   *
79   *  &lt;commands&gt;
80   *     &lt;command name="subscribe" class="Subscribe"/&gt;
81   *     &lt;command name="subscribe-confirm" class="SubscribeConfirm"/&gt;
82   *     &lt;command name="unsubscribe" class="UnSubscribe"/&gt;
83   *     &lt;command name="unsubscribe-confirm" class="UnSubscribeConfirm"/&gt;
84   *     &lt;command name="error" class="ErrorCommand"/&gt;
85   *     &lt;command name="owner" class="Owner"/&gt;
86   *     &lt;command name="info" class="Info"/&gt;
87   *  &lt;/commands&gt;
88   * &lt;/mailet&gt;
89   * </pre>
90   *
91   * <br />
92   * <br />
93   * Todo: refine the command matching so we can have more sophistciated commands such as:
94   * <pre>
95   * &lt;listName&gt;-&lt;commandName&gt;-&lt;optCommandParam&gt;@domain
96   * </pre>
97   *
98   * @version CVS $Revision: 430699 $ $Date: 2006-08-11 07:02:35 +0000 (ven, 11 ago 2006) $
99   * @since 2.2.0
100  */
101 public class CommandListservManager extends GenericMailet implements ICommandListservManager {
102 
103     protected Map commandMap = new HashMap();
104     protected List commandPackages = new ArrayList();
105     protected UsersRepository usersRepository;
106     protected String listName;
107     protected String displayName;
108     protected String listOwner;
109     protected String listDomain;
110     protected XMLResources xmlResources;
111 
112     /***
113      * Get the name of this list specified by the config param: 'listName'.
114      * <br />
115      * eg: <pre>&lt;listName&gt;announce&lt;/listName&gt;</pre>
116      *
117      * @param displayFormat is whether you want a display version of this or not
118      * @return the official display name of this list
119      */
120     public String getListName(boolean displayFormat) {
121         return displayFormat ? displayName : listName;
122     }
123 
124     /***
125      * Gets the owner of this list specified by the config param: 'listOwner'.
126      * <br />
127      * eg: <pre>&lt;listOwner&gt;owner@localhost&lt;/listOwner&gt;</pre>
128      *
129      * @return this is an address like listOwner@localhost
130      */
131     public String getListOwner() {
132         return listOwner;
133     }
134 
135     /***
136      * Get the domain of the list specified by the config param: 'listDomain'.
137      * <br />
138      * eg: <pre>&lt;listDomain&gt;localhost&lt;/listDomain&gt;</pre>
139      *
140      * @return a string like localhost
141      */
142     public String getListDomain() {
143         return listDomain;
144     }
145 
146     /***
147      * Get a specific command specified by the 'commands' configuration block.
148      * For instance:
149      * <pre>
150      * &lt;commands&gt;
151      *  &lt;command name="subscribe" class="Subscribe"/&gt;
152      *  &lt;command name="subscribe-confirm" class="SubscribeConfirm"/&gt;
153      *  &lt;command name="unsubscribe" class="UnSubscribe"/&gt;
154      *  &lt;command name="unsubscribe-confirm" class="UnSubscribeConfirm"/&gt;
155      *  &lt;command name="error" class="ErrorCommand"/&gt;
156      *  &lt;command name="owner" class="Owner"/&gt;
157      *  &lt;command name="info" class="Info"/&gt;
158      * &lt;/commands&gt;
159      * </pre>
160      * @param name case in-sensitive
161      * @return a {@link IListServCommand} if found, null otherwise
162      */
163     public IListServCommand getCommand(String name) {
164         return (IListServCommand) commandMap.get(name.toLowerCase(Locale.US));
165     }
166 
167     /***
168      * Get all the available commands
169      * @return a map of {@link IListServCommand}
170      * @see #getCommand
171      */
172     public Map getCommands() {
173         return commandMap;
174     }
175 
176     /***
177      * Get the current user repository for this list serv
178      * @return an instance of {@link UsersRepository} that is used for the member list of the list serv
179      */
180     public UsersRepository getUsersRepository() {
181         return usersRepository;
182     }
183 
184     /***
185      * An error occurred, send some sort of message
186      * @param subject the subject of the message to send
187      * @param mail
188      * @param errorMessage
189      */
190     public void onError(Mail mail, String subject, String errorMessage) throws MessagingException {
191         ErrorCommand errorCommand = (ErrorCommand) getCommand("error");
192         errorCommand.onError(mail, subject, errorMessage);
193     }
194 
195     /***
196      * @return the configuration file for the xml resources
197      */
198     public String getResourcesFile() {
199         return getInitParameter("resources");
200     }
201 
202     /***
203      * Use this to get standard properties for future calls to {@link org.apache.james.util.XMLResources}
204      * @return properties with the "LIST_NAME" and the "DOMAIN_NAME" properties
205      */
206     public Properties getStandardProperties() {
207         Properties standardProperties = new Properties();
208         standardProperties.put("LIST_NAME", getListName(false));
209         standardProperties.put("DISPLAY_NAME", getListName(true));
210         standardProperties.put("DOMAIN_NAME", getListDomain());
211         return standardProperties;
212     }
213 
214     /***
215      * Initializes an array of resources
216      * @param names such as 'header, footer' etc...
217      * @return an initialized array of XMLResources
218      * @throws ConfigurationException
219      */
220     public XMLResources[] initXMLResources(String[] names) throws ConfigurationException {
221         try {
222             File xmlFile = new File(getResourcesFile());
223 
224             Properties props = getStandardProperties();
225             String listName = props.getProperty("LIST_NAME");
226 
227             XMLResources[] xmlResources = new XMLResources[names.length];
228             for (int index = 0; index < names.length; index++) {
229                 xmlResources[index] = new XMLResources();
230                 xmlResources[index].init(xmlFile, names[index], listName, props);
231             }
232             return xmlResources;
233         } catch (Exception e) {
234             log(e.getMessage(), e);
235             throw new ConfigurationException("Can't initialize:", e);
236         }
237     }
238 
239     public void init() throws MessagingException {
240 
241         try {
242             //Well, i want a more complex configuration structure
243             //of my mailet, so i have to cheat... and cheat i will...
244             Configuration configuration = (Configuration) getField(getMailetConfig(), "configuration");
245 
246             //get name
247             listName = configuration.getChild("listName").getValue();
248             displayName = configuration.getChild("displayName").getValue();
249             listOwner = configuration.getChild("listOwner").getValue();
250             listDomain = configuration.getChild("listDomain").getValue();
251 
252             //initialize resources
253             initializeResources();
254 
255             //get users store
256             initUsersRepository();
257 
258             //get command packages
259             loadCommandPackages(configuration);
260 
261             //load commands
262             loadCommands(configuration);
263 
264             //register w/context
265             getMailetContext().setAttribute(ICommandListservManager.ID + listName, this);
266         } catch (Exception e) {
267             throw new MessagingException(e.getMessage(), e);
268         }
269     }
270 
271     /***
272      * Based on the to address get a valid or command or null
273      * @param mailAddress
274      * @return IListServCommand or null
275      */
276     public IListServCommand getCommandTarget(MailAddress mailAddress) {
277         String commandName = getCommandName(mailAddress);
278         return getCommand(commandName);
279     }
280 
281     /***
282      * <p>Called by the mailet container to allow the mailet to process a
283      * message.</p>
284      *
285      * <p>This method is declared abstract so subclasses must override it.</p>
286      *
287      * @param mail - the Mail object that contains the MimeMessage and
288      *          routing information
289      * @throws MessagingException - if an exception occurs that interferes with the mailet's normal operation
290      *          occurred
291      */
292     public void service(Mail mail) throws MessagingException {
293         if (mail.getRecipients().size() != 1) {
294             getMailetContext().bounce(mail, "You can only send one command at a time to this listserv manager.");
295             return;
296         }
297         MailAddress mailAddress = (MailAddress) mail.getRecipients().iterator().next();
298         IListServCommand command = getCommandTarget(mailAddress);
299 
300         if (command == null) {
301             //don't recognize the command
302             Properties props = getStandardProperties();
303             props.setProperty("COMMAND", getCommandName(mailAddress));
304             onError(mail, "unknown command", xmlResources.getString("command.not.understood", props));
305         } else {
306             command.onCommand(mail);
307         }
308 
309         // onError or onCommand would have done the job, so regardless
310         // of which get rid of this e-mail.  This is something that we
311         // should review, and decide if there is any reason to allow a
312         // passthrough.
313         mail.setState(Mail.GHOST);
314     }
315 
316     /***
317      * Get the name of the command
318      * @param mailAddress
319      * @return the name of the command
320      */
321     protected String getCommandName(MailAddress mailAddress) {
322         String user = mailAddress.getUser();
323         int index = user.indexOf('-', listName.length());
324         String commandName = user.substring(++index);
325         return commandName;
326     }
327 
328     /***
329      * initialize the resources
330      * @throws Exception
331      */
332     protected void initializeResources() throws Exception {
333         xmlResources = initXMLResources(new String[]{"List Manager"})[0];
334     }
335 
336     /***
337      * Fetch the repository of users
338      */
339     protected void initUsersRepository() {
340         ServiceManager compMgr = (ServiceManager) getMailetContext().getAttribute(Constants.AVALON_COMPONENT_MANAGER);
341         try {
342             UsersStore usersStore = (UsersStore) compMgr.lookup(UsersStore.ROLE);
343             String repName = getInitParameter("repositoryName");
344 
345             usersRepository = usersStore.getRepository(repName);
346         } catch (Exception e) {
347             log("Failed to retrieve Store component:" + e.getMessage());
348         }
349     }
350 
351     /***
352      * Load an initialize all of the available commands
353      * @param configuration
354      * @throws ConfigurationException
355      */
356     protected void loadCommands(Configuration configuration) throws Exception {
357         final Configuration commandConfigurations = configuration.getChild("commands");
358         final Configuration[] commandConfs = commandConfigurations.getChildren("command");
359         for (int index = 0; index < commandConfs.length; index++) {
360             Configuration commandConf = commandConfs[index];
361             String commandName = commandConf.getAttribute("name").toLowerCase();
362             String className = commandConf.getAttribute("class");
363             loadCommand(commandName, className, commandConf);
364         }
365     }
366 
367     /***
368      * Loads and initializes a single command
369      *
370      * @param commandName
371      * @param className
372      * @param configuration
373      * @throws ConfigurationException
374      */
375     protected void loadCommand(String commandName,
376                                String className,
377                                Configuration configuration)
378             throws ConfigurationException, ClassNotFoundException, IllegalAccessException, InstantiationException {
379         ClassLoader theClassLoader = getClass().getClassLoader();
380         for (Iterator it = commandPackages.iterator(); it.hasNext();) {
381             String packageName = (String) it.next();
382 
383             IListServCommand listServCommand = null;
384             try {
385                 listServCommand = (IListServCommand) theClassLoader.loadClass(packageName + className).newInstance();
386             } catch (Exception e) {
387                 //ignore
388                 continue;
389             }
390             listServCommand.init(this, configuration);
391             commandMap.put(commandName, listServCommand);
392             return;
393         }
394 
395         throw new ConfigurationException("Unable to load listservcommand: " + commandName);
396     }
397 
398     /***
399      * loads all of the packages for the commands
400      *
401      * @param configuration
402      * @throws ConfigurationException
403      */
404     protected void loadCommandPackages(Configuration configuration) throws ConfigurationException {
405         commandPackages.add("");
406         final Configuration packageConfiguration = configuration.getChild("commandpackages");
407         final Configuration[] pkgConfs = packageConfiguration.getChildren("commandpackage");
408         for (int index = 0; index < pkgConfs.length; index++) {
409             Configuration conf = pkgConfs[index];
410             String packageName = conf.getValue().trim();
411             if (!packageName.endsWith(".")) {
412                 packageName += ".";
413             }
414             commandPackages.add(packageName);
415         }
416     }
417 
418     /***
419      * Retrieves a data field, potentially defined by a super class.
420      * @return null if not found, the object otherwise
421      */
422     protected static Object getField(Object instance, String name) throws IllegalAccessException {
423         Class clazz = instance.getClass();
424         Field[] fields;
425         while (clazz != null) {
426             fields = clazz.getDeclaredFields();
427             for (int index = 0; index < fields.length; index++) {
428                 Field field = fields[index];
429                 if (field.getName().equals(name)) {
430                     field.setAccessible(true);
431                     return field.get(instance);
432                 }
433             }
434             clazz = clazz.getSuperclass();
435         }
436 
437         return null;
438     }
439 }