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