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 * <mailet match="RecipientIs=announce@localhost" class="CommandListservProcessor">
55 * <membersonly>false</membersonly>
56 * <attachmentsallowed>true</attachmentsallowed>
57 * <replytolist>true</replytolist>
58 * <repositoryName>list-announce</repositoryName>
59 * <subjectprefix>Announce</subjectprefix>
60 * <autobracket>true</autobracket>
61 * <listOwner>owner@localhost</listOwner>
62 * <listName>announce</listName>
63 * </mailet>
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><membersonly>false</membersonly></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><attachmentsallowed>true</attachmentsallowed></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><replytolist>true</replytolist></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><subjectPrefix>MyList</subjectPrefix></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><autoBracket>true</autoBracket></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><repositoryName>list-announce</repositoryName></pre>
125 */
126 protected UsersRepository usersRepository;
127
128 /***
129 * The list owner
130 * specified by the config param: 'listOwner'.
131 * <br />
132 * eg: <pre><listOwner>owner@localhost</listOwner></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><listName>announce</listName></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
179 initializeResources();
180
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
199 if (!checkAllowedPoster(mail, members)) {
200 return;
201 }
202
203
204 if (!checkAnnouncements(mail)) {
205 return;
206 }
207
208
209 if (!checkBeenThere(listservAddr, mail)) {
210 return;
211 }
212
213
214 addFooter(mail);
215
216
217 MimeMessage message = prepareListMessage(mail, listservAddr);
218
219
220 setSubject(message);
221
222
223
224 getMailetContext().sendMail(listOwner, members, message);
225 } catch (IOException ioe) {
226 throw new MailetException("Error creating listserv message", ioe);
227 } finally {
228
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
271 MimeMessage message = new MimeMessage(mail.getMessage());
272
273
274 message.removeHeader(RFC2822Headers.RETURN_PATH);
275
276
277
278 message.setHeader("X-been-there", listservAddr.toString());
279
280
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
353
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
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
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 * <prefix> + <one-optional-"Re:"*gt; + <remaining subject>
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
435
436
437
438 StringBuffer subject = new StringBuffer(subj);
439 int prefixLength = prefix.length();
440
441
442
443
444 int index = subject.toString().indexOf(prefix);
445 if (index != 0) {
446
447 if (index > 0) {
448 subject.delete(index, index + prefixLength);
449 }
450 subject.insert(0, prefix);
451 }
452
453
454 String match = "Re:";
455 index = subject.toString().indexOf(match, prefixLength);
456
457 while(index > -1) {
458
459 subject.replace(index, index + match.length(), "RE:");
460 index = subject.toString().indexOf(match, prefixLength);
461
462 }
463
464
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
470 subject.delete(index, index + match.length());
471 index = subject.toString().indexOf(match, indexRE);
472
473 }
474
475
476 match = " ";
477 index = subject.toString().indexOf(match, prefixLength);
478 while(index > -1) {
479
480 subject.replace(index, index + match.length(), " ");
481 index = subject.toString().indexOf(match, prefixLength);
482
483 }
484
485
486
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
563
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 }