001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.transport.mailets;
019:
020: import org.apache.avalon.framework.service.ServiceManager;
021: import org.apache.avalon.framework.configuration.Configuration;
022: import org.apache.james.Constants;
023: import org.apache.james.services.UsersRepository;
024: import org.apache.james.services.UsersStore;
025: import org.apache.mailet.RFC2822Headers;
026: import org.apache.james.util.XMLResources;
027: import org.apache.mailet.GenericMailet;
028: import org.apache.mailet.Mail;
029: import org.apache.mailet.MailAddress;
030: import org.apache.mailet.MailetException;
031:
032: import javax.mail.MessagingException;
033: import javax.mail.internet.MimeMessage;
034: import javax.mail.internet.MimeMultipart;
035: import javax.mail.internet.ParseException;
036: import java.io.IOException;
037: import java.lang.reflect.Field;
038: import java.util.ArrayList;
039: import java.util.Collection;
040: import java.util.Iterator;
041: import java.util.Properties;
042:
043: /**
044: * CommandListservProcessor processes messages intended for the list serv mailing list.
045: * For command handling, see {@link CommandListservManager} <br />
046: *
047: * This class is based on the existing list serv processor shipped with James.
048: * <br />
049: * <br />
050: *
051: * To configure the CommandListservProcessor place this configuratin in the root processor:
052: * <pre>
053: * <mailet match="RecipientIs=announce@localhost" class="CommandListservProcessor">
054: * <membersonly>false</membersonly>
055: * <attachmentsallowed>true</attachmentsallowed>
056: * <replytolist>true</replytolist>
057: * <repositoryName>list-announce</repositoryName>
058: * <subjectprefix>Announce</subjectprefix>
059: * <autobracket>true</autobracket>
060: * <listOwner>owner@localhost</listOwner>
061: * <listName>announce</listName>
062: * </mailet>
063: *
064: * </pre>
065: *
066: * @version CVS $Revision: 494012 $ $Date: 2007-01-08 11:23:58 +0100 (Mo, 08 Jan 2007) $
067: * @since 2.2.0
068: */
069: public class CommandListservProcessor extends GenericMailet {
070:
071: /**
072: * Whether only members can post to the list specified by the config param: 'membersonly'.
073: * <br />
074: * eg: <pre><membersonly>false</membersonly></pre>
075: *
076: * Defaults to false
077: */
078: protected boolean membersOnly;
079:
080: /**
081: * Whether attachments can be sent to the list specified by the config param: 'attachmentsallowed'.
082: * <br />
083: * eg: <pre><attachmentsallowed>true</attachmentsallowed></pre>
084: *
085: * Defaults to true
086: */
087: protected boolean attachmentsAllowed;
088:
089: /**
090: * Whether the reply-to header should be set to the list address
091: * specified by the config param: 'replytolist'.
092: * <br />
093: * eg: <pre><replytolist>true</replytolist></pre>
094: *
095: * Defaults to true
096: */
097: protected boolean replyToList;
098:
099: /**
100: * A String to prepend to the subject of the message when it is sent to the list
101: * specified by the config param: 'subjectPrefix'.
102: * <br />
103: * eg: <pre><subjectPrefix>MyList</subjectPrefix></pre>
104: *
105: * For example: MyList
106: */
107: protected String subjectPrefix;
108:
109: /**
110: * Whether the subject prefix should be bracketed with '[' and ']'
111: * specified by the config param: 'autoBracket'.
112: * <br />
113: * eg: <pre><autoBracket>true</autoBracket></pre>
114: *
115: * Defaults to true
116: */
117: protected boolean autoBracket;
118:
119: /**
120: * The repository containing the users on this list
121: * specified by the config param: 'repositoryName'.
122: * <br />
123: * eg: <pre><repositoryName>list-announce</repositoryName></pre>
124: */
125: protected UsersRepository usersRepository;
126:
127: /**
128: * The list owner
129: * specified by the config param: 'listOwner'.
130: * <br />
131: * eg: <pre><listOwner>owner@localhost</listOwner></pre>
132: */
133: protected MailAddress listOwner;
134:
135: /**
136: * Name of the mailing list
137: * specified by the config param: 'listName'.
138: * <br />
139: * eg: <pre><listName>announce</listName></pre>
140: *
141: */
142: protected String listName;
143:
144: /**
145: * The list serv manager
146: */
147: protected ICommandListservManager commandListservManager;
148:
149: /**
150: * Mailet that will add the footer to the message
151: */
152: protected CommandListservFooter commandListservFooter;
153:
154: /**
155: * @see XMLResources
156: */
157: protected XMLResources xmlResources;
158:
159: protected boolean specificPostersOnly;
160: protected Collection allowedPosters;
161:
162: /**
163: * Initialize the mailet
164: */
165: public void init() throws MessagingException {
166: try {
167: Configuration configuration = (Configuration) getField(
168: 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",
178: false);
179: //initialize resources
180: initializeResources();
181: //init user repos
182: initUsersRepository();
183: initAllowedPosters(configuration);
184: } catch (Exception e) {
185: throw new MessagingException(e.getMessage(), e);
186: }
187: }
188:
189: /**
190: * A message was sent to the list serv. Broadcast if appropriate...
191: * @param mail
192: * @throws MessagingException
193: */
194: public void service(Mail mail) throws MessagingException {
195: try {
196: Collection members = getMembers();
197: MailAddress listservAddr = (MailAddress) mail
198: .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(
229: "Error creating listserv message", ioe);
230: } finally {
231: //Kill the old message
232: mail.setState(Mail.GHOST);
233: }
234: }
235:
236: /**
237: * Add the footer using {@link CommandListservFooter}
238: * @param mail
239: * @throws MessagingException
240: */
241: protected void addFooter(Mail mail) throws MessagingException {
242: getCommandListservFooter().service(mail);
243: }
244:
245: protected void setSubject(MimeMessage message)
246: throws MessagingException {
247: String prefix = subjectPrefix;
248: if (prefix != null) {
249: if (autoBracket) {
250: StringBuffer prefixBuffer = new StringBuffer(64)
251: .append("[").append(prefix).append("]");
252: prefix = prefixBuffer.toString();
253: }
254: String subj = message.getSubject();
255: if (subj == null) {
256: subj = "";
257: }
258: subj = normalizeSubject(subj, prefix);
259: AbstractRedirect.changeSubject(message, subj);
260: }
261: }
262:
263: /**
264: * Create a new message with some set headers
265: * @param mail
266: * @param listservAddr
267: * @return a prepared List Message
268: * @throws MessagingException
269: */
270: protected MimeMessage prepareListMessage(Mail mail,
271: 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
285: .toString());
286: }
287:
288: return message;
289: }
290:
291: /**
292: * return true if this is ok, false otherwise
293: * Check if the X-been-there header is set to the listserv's name
294: * (the address). If it has, this means it's a message from this
295: * listserv that's getting bounced back, so we need to swallow it
296: *
297: * @param listservAddr
298: * @param mail
299: * @return true if this message has already bounced, false otherwse
300: * @throws MessagingException
301: */
302: protected boolean checkBeenThere(MailAddress listservAddr, Mail mail)
303: throws MessagingException {
304: if (listservAddr.equals(mail.getMessage().getHeader(
305: "X-been-there"))) {
306: return false;
307: }
308: return true;
309: }
310:
311: /**
312: * Returns true if this is ok to send to the list
313: * @param mail
314: * @return true if this message is ok, false otherwise
315: * @throws IOException
316: * @throws MessagingException
317: */
318: protected boolean checkAnnouncements(Mail mail) throws IOException,
319: MessagingException {
320: if (!attachmentsAllowed
321: && mail.getMessage().getContent() instanceof MimeMultipart) {
322: Properties standardProperties = getCommandListservManager()
323: .getStandardProperties();
324:
325: getCommandListservManager().onError(
326: mail,
327: xmlResources.getString("invalid.mail.subject",
328: standardProperties),
329: xmlResources.getString("error.attachments",
330: standardProperties));
331: return false;
332: }
333: return true;
334: }
335:
336: /**
337: * Returns true if this user is ok to send to the list
338: *
339: * @param members
340: * @param mail
341: * @return true if this message is ok, false otherwise
342: * @throws MessagingException
343: */
344: protected boolean checkMembers(Collection members, Mail mail)
345: throws MessagingException {
346: if (membersOnly && !members.contains(mail.getSender())) {
347: Properties standardProperties = getCommandListservManager()
348: .getStandardProperties();
349: getCommandListservManager().onError(
350: mail,
351: xmlResources.getString("invalid.mail.subject",
352: standardProperties),
353: xmlResources.getString("error.membersonly",
354: standardProperties));
355:
356: return false;
357: }
358: return true;
359: }
360:
361: public Collection getMembers() throws ParseException {
362: Collection reply = new ArrayList();
363: for (Iterator it = usersRepository.list(); it.hasNext();) {
364: String member = it.next().toString();
365: try {
366: reply.add(new MailAddress(member));
367: } catch (Exception e) {
368: // Handle an invalid subscriber address by logging it and
369: // proceeding to the next member.
370: StringBuffer logBuffer = new StringBuffer(1024).append(
371: "Invalid subscriber address: ").append(member)
372: .append(" caused: ").append(e.getMessage());
373: log(logBuffer.toString());
374: }
375: }
376: return reply;
377: }
378:
379: /**
380: * Get a configuration value
381: * @param attrName
382: * @param defValue
383: * @return the value if found, defValue otherwise
384: */
385: protected boolean getBoolean(String attrName, boolean defValue) {
386: boolean value = defValue;
387: try {
388: value = new Boolean(getInitParameter(attrName))
389: .booleanValue();
390: } catch (Exception e) {
391: // Ignore any exceptions, default to false
392: }
393: return value;
394: }
395:
396: /**
397: * Get a configuration value
398: * @param attrName
399: * @param defValue
400: * @return the attrValue if found, defValue otherwise
401: */
402: protected String getString(String attrName, String defValue) {
403: String value = defValue;
404: try {
405: value = getInitParameter(attrName);
406: } catch (Exception e) {
407: // Ignore any exceptions, default to false
408: }
409: return value;
410: }
411:
412: /**
413: * initialize the resources
414: * @throws Exception
415: */
416: protected void initializeResources() throws Exception {
417: xmlResources = getCommandListservManager().initXMLResources(
418: new String[] { "List Manager" })[0];
419: }
420:
421: /**
422: * Fetch the repository of users
423: */
424: protected void initUsersRepository() throws Exception {
425: ServiceManager compMgr = (ServiceManager) getMailetContext()
426: .getAttribute(Constants.AVALON_COMPONENT_MANAGER);
427: UsersStore usersStore = (UsersStore) compMgr
428: .lookup(UsersStore.ROLE);
429: String repName = getInitParameter("repositoryName");
430:
431: usersRepository = usersStore.getRepository(repName);
432: if (usersRepository == null)
433: throw new Exception("Invalid user repository: " + repName);
434: }
435:
436: /**
437: * <p>This takes the subject string and reduces (normailzes) it.
438: * Multiple "Re:" entries are reduced to one, and capitalized. The
439: * prefix is always moved/placed at the beginning of the line, and
440: * extra blanks are reduced, so that the output is always of the
441: * form:</p>
442: * <code>
443: * <prefix> + <one-optional-"Re:"*gt; + <remaining subject>
444: * </code>
445: * <p>I have done extensive testing of this routine with a standalone
446: * driver, and am leaving the commented out debug messages so that
447: * when someone decides to enhance this method, it can be yanked it
448: * from this file, embedded it with a test driver, and the comments
449: * enabled.</p>
450: */
451: static private String normalizeSubject(final String subj,
452: final String prefix) {
453: // JDK IMPLEMENTATION NOTE! When we require JDK 1.4+, all
454: // occurrences of subject.toString.().indexOf(...) can be
455: // replaced by subject.indexOf(...).
456:
457: StringBuffer subject = new StringBuffer(subj);
458: int prefixLength = prefix.length();
459:
460: // System.err.println("In: " + subject);
461:
462: // If the "prefix" is not at the beginning the subject line, remove it
463: int index = subject.toString().indexOf(prefix);
464: if (index != 0) {
465: // System.err.println("(p) index: " + index + ", subject: " + subject);
466: if (index > 0) {
467: subject.delete(index, index + prefixLength);
468: }
469: subject.insert(0, prefix); // insert prefix at the front
470: }
471:
472: // Replace Re: with RE:
473: String match = "Re:";
474: index = subject.toString().indexOf(match, prefixLength);
475:
476: while (index > -1) {
477: // System.err.println("(a) index: " + index + ", subject: " + subject);
478: subject.replace(index, index + match.length(), "RE:");
479: index = subject.toString().indexOf(match, prefixLength);
480: // System.err.println("(b) index: " + index + ", subject: " + subject);
481: }
482:
483: // Reduce them to one at the beginning
484: match = "RE:";
485: int indexRE = subject.toString().indexOf(match, prefixLength)
486: + match.length();
487: index = subject.toString().indexOf(match, indexRE);
488: while (index > 0) {
489: // System.err.println("(c) index: " + index + ", subject: " + subject);
490: subject.delete(index, index + match.length());
491: index = subject.toString().indexOf(match, indexRE);
492: // System.err.println("(d) index: " + index + ", subject: " + subject);
493: }
494:
495: // Reduce blanks
496: match = " ";
497: index = subject.toString().indexOf(match, prefixLength);
498: while (index > -1) {
499: // System.err.println("(e) index: " + index + ", subject: " + subject);
500: subject.replace(index, index + match.length(), " ");
501: index = subject.toString().indexOf(match, prefixLength);
502: // System.err.println("(f) index: " + index + ", subject: " + subject);
503: }
504:
505: // System.err.println("Out: " + subject);
506:
507: return subject.toString();
508: }
509:
510: /**
511: * lazy retrieval
512: * @return ICommandListservManager
513: */
514: protected ICommandListservManager getCommandListservManager() {
515: if (commandListservManager == null) {
516: commandListservManager = (ICommandListservManager) getMailetContext()
517: .getAttribute(ICommandListservManager.ID + listName);
518: if (commandListservManager == null) {
519: throw new IllegalStateException(
520: "Unable to find command list manager named: "
521: + listName);
522: }
523: }
524:
525: return commandListservManager;
526: }
527:
528: /**
529: * Lazy init
530: * @throws MessagingException
531: */
532: protected CommandListservFooter getCommandListservFooter()
533: throws MessagingException {
534: if (commandListservFooter == null) {
535: commandListservFooter = new CommandListservFooter(
536: getCommandListservManager());
537: commandListservFooter.init(getMailetConfig());
538: }
539: return commandListservFooter;
540: }
541:
542: /**
543: * Retrieves a data field, potentially defined by a super class.
544: * @return null if not found, the object otherwise
545: */
546: protected static Object getField(Object instance, String name)
547: throws IllegalAccessException {
548: Class clazz = instance.getClass();
549: Field[] fields;
550: while (clazz != null) {
551: fields = clazz.getDeclaredFields();
552: for (int index = 0; index < fields.length; index++) {
553: Field field = fields[index];
554: if (field.getName().equals(name)) {
555: field.setAccessible(true);
556: return field.get(instance);
557: }
558: }
559: clazz = clazz.getSuperclass();
560: }
561:
562: return null;
563: }
564:
565: protected void initAllowedPosters(Configuration configuration)
566: throws Exception {
567: final Configuration allowedPostersElement = configuration
568: .getChild("allowedposters");
569: allowedPosters = new ArrayList();
570: if (allowedPostersElement != null) {
571: final Configuration[] addresses = allowedPostersElement
572: .getChildren("address");
573: for (int index = 0; index < addresses.length; index++) {
574: Configuration address = addresses[index];
575: String emailAddress = address.getValue();
576: allowedPosters.add(new MailAddress(emailAddress));
577: }
578: }
579: }
580:
581: /**
582: * Returns true if this user is ok to send to the list
583: *
584: * @param mail
585: * @return true if this message is ok, false otherwise
586: * @throws MessagingException
587: */
588: protected boolean checkAllowedPoster(Mail mail, Collection members)
589: throws MessagingException {
590: /*
591: 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.
592: if the sender is in the allowed list, post
593: */
594: if ((!specificPostersOnly && (!membersOnly || members
595: .contains(mail.getSender())))
596: || allowedPosters.contains(mail.getSender())) {
597: return true;
598: } else {
599: Properties standardProperties = getCommandListservManager()
600: .getStandardProperties();
601: getCommandListservManager().onError(
602: mail,
603: xmlResources.getString("invalid.mail.subject",
604: standardProperties),
605: xmlResources.getString("error.membersonly",
606: standardProperties));
607: return false;
608: }
609: }
610: }
|