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.james.core.MailImpl;
021: import org.apache.james.util.XMLResources;
022: import org.apache.mailet.GenericMailet;
023: import org.apache.mailet.Mail;
024: import org.apache.mailet.MailAddress;
025: import org.apache.oro.text.regex.MatchResult;
026: import org.apache.oro.text.regex.Pattern;
027: import org.apache.oro.text.regex.Perl5Compiler;
028: import org.apache.oro.text.regex.Perl5Matcher;
029:
030: import javax.mail.MessagingException;
031: import javax.mail.internet.ParseException;
032:
033: import java.util.ArrayList;
034: import java.util.Collection;
035: import java.util.HashMap;
036: import java.util.HashSet;
037: import java.util.Iterator;
038: import java.util.Map;
039: import java.util.StringTokenizer;
040:
041: /**
042: * Provides an abstraction of common functionality needed for implementing
043: * a Virtual User Table. Override the <code>mapRecipients</code> method to
044: * map virtual recipients to real recipients.
045: */
046: public abstract class AbstractVirtualUserTable extends GenericMailet {
047: static private final String MARKER = "org.apache.james.transport.mailets.AbstractVirtualUserTable.mapped";
048:
049: /**
050: * Checks the recipient list of the email for user mappings. Maps recipients as
051: * appropriate, modifying the recipient list of the mail and sends mail to any new
052: * non-local recipients.
053: *
054: * @param mail the mail to process
055: */
056: public void service(Mail mail) throws MessagingException {
057: if (mail.getAttribute(MARKER) != null) {
058: mail.removeAttribute(MARKER);
059: return;
060: }
061:
062: Collection recipientsToRemove = new HashSet();
063: Collection recipientsToAddLocal = new ArrayList();
064: Collection recipientsToAddForward = new ArrayList();
065:
066: Collection recipients = mail.getRecipients();
067: Map recipientsMap = new HashMap(recipients.size());
068:
069: for (Iterator iter = recipients.iterator(); iter.hasNext();) {
070: MailAddress address = (MailAddress) iter.next();
071:
072: // Assume all addresses are non-virtual at start
073: recipientsMap.put(address, null);
074: }
075:
076: mapRecipients(recipientsMap);
077:
078: for (Iterator iter = recipientsMap.keySet().iterator(); iter
079: .hasNext();) {
080: MailAddress source = (MailAddress) iter.next();
081: String targetString = (String) recipientsMap.get(source);
082:
083: // Only non-null mappings are translated
084: if (targetString != null) {
085: if (targetString.startsWith("error:")) {
086: //Mark this source address as an address to remove from the recipient list
087: recipientsToRemove.add(source);
088: processDSN(mail, source, targetString);
089: } else {
090: StringTokenizer tokenizer = new StringTokenizer(
091: targetString, getSeparator(targetString));
092:
093: while (tokenizer.hasMoreTokens()) {
094: String targetAddress = tokenizer.nextToken()
095: .trim();
096:
097: // log("Attempting to map from " + source + " to " + targetAddress);
098:
099: if (targetAddress.startsWith("regex:")) {
100: targetAddress = regexMap(mail, source,
101: targetAddress);
102: if (targetAddress == null)
103: continue;
104: }
105:
106: try {
107: MailAddress target = (targetAddress
108: .indexOf('@') < 0) ? new MailAddress(
109: targetAddress, "localhost")
110: : new MailAddress(targetAddress);
111:
112: //Mark this source address as an address to remove from the recipient list
113: recipientsToRemove.add(source);
114:
115: // We need to separate local and remote
116: // recipients. This is explained below.
117: if (getMailetContext().isLocalServer(
118: target.getHost())) {
119: recipientsToAddLocal.add(target);
120: } else {
121: recipientsToAddForward.add(target);
122: }
123:
124: StringBuffer buf = new StringBuffer()
125: .append("Translating virtual user ")
126: .append(source).append(" to ")
127: .append(target);
128: log(buf.toString());
129:
130: } catch (ParseException pe) {
131: //Don't map this address... there's an invalid address mapping here
132: StringBuffer exceptionBuffer = new StringBuffer(
133: 128).append(
134: "There is an invalid map from ")
135: .append(source).append(" to ")
136: .append(targetAddress);
137: log(exceptionBuffer.toString());
138: continue;
139: }
140: }
141: }
142: }
143: }
144:
145: // Remove mapped recipients
146: recipients.removeAll(recipientsToRemove);
147:
148: // Add mapped recipients that are local
149: recipients.addAll(recipientsToAddLocal);
150:
151: // We consider an address that we map to be, by definition, a
152: // local address. Therefore if we mapped to a remote address,
153: // then we want to make sure that the mail can be relayed.
154: // However, the original e-mail would typically be subjected to
155: // relay testing. By posting a new mail back through the
156: // system, we have a locally generated mail, which will not be
157: // subjected to relay testing.
158:
159: // Forward to mapped recipients that are remote
160: if (recipientsToAddForward.size() != 0) {
161: // Can't use this ... some mappings could lead to an infinite loop
162: // getMailetContext().sendMail(mail.getSender(), recipientsToAddForward, mail.getMessage());
163:
164: // duplicates the Mail object, to be able to modify the new mail keeping the original untouched
165: MailImpl newMail = new MailImpl(mail, newName(mail));
166: try {
167: try {
168: newMail.setRemoteAddr(java.net.InetAddress
169: .getLocalHost().getHostAddress());
170: newMail.setRemoteHost(java.net.InetAddress
171: .getLocalHost().getHostName());
172: } catch (java.net.UnknownHostException _) {
173: newMail.setRemoteAddr("127.0.0.1");
174: newMail.setRemoteHost("localhost");
175: }
176: newMail.setRecipients(recipientsToAddForward);
177: newMail.setAttribute(MARKER, Boolean.TRUE);
178: getMailetContext().sendMail(newMail);
179: } finally {
180: newMail.dispose();
181: }
182: }
183:
184: // If there are no recipients left, Ghost the message
185: if (recipients.size() == 0) {
186: mail.setState(Mail.GHOST);
187: }
188: }
189:
190: /**
191: * Override to map virtual recipients to real recipients, both local and non-local.
192: * Each key in the provided map corresponds to a potential virtual recipient, stored as
193: * a <code>MailAddress</code> object.
194: *
195: * Translate virtual recipients to real recipients by mapping a string containing the
196: * address of the real recipient as a value to a key. Leave the value <code>null<code>
197: * if no mapping should be performed. Multiple recipients may be specified by delineating
198: * the mapped string with commas, semi-colons or colons.
199: *
200: * @param recipientsMap the mapping of virtual to real recipients, as
201: * <code>MailAddress</code>es to <code>String</code>s.
202: */
203: protected abstract void mapRecipients(Map recipientsMap)
204: throws MessagingException;
205:
206: /**
207: * Sends the message for DSN processing
208: *
209: * @param mail the Mail instance being processed
210: * @param address the MailAddress causing the DSN
211: * @param error a String in the form "error:<code> <msg>"
212: */
213: private void processDSN(Mail mail, MailAddress address, String error) {
214: // parse "error:<code> <msg>"
215: int msgPos = error.indexOf(' ');
216: try {
217: Integer code = Integer.valueOf(error.substring("error:"
218: .length(), msgPos));
219: } catch (NumberFormatException e) {
220: log("Cannot send DSN. Exception parsing DSN code from: "
221: + error, e);
222: return;
223: }
224: String msg = error.substring(msgPos + 1);
225: // process bounce for "source" address
226: try {
227: getMailetContext().bounce(mail, error);
228: } catch (MessagingException me) {
229: log("Cannot send DSN. Exception during DSN processing: ",
230: me);
231: }
232: }
233:
234: /**
235: * Processes regex virtual user mapping
236: *
237: * If a mapped target string begins with the prefix regex:, it must be
238: * formatted as regex:<regular-expression>:<parameterized-string>,
239: * e.g., regex:(.*)@(.*):${1}@tld
240: *
241: * @param mail the Mail instance being processed
242: * @param address the MailAddress to be mapped
243: * @param targetString a String specifying the mapping
244: */
245: private String regexMap(Mail mail, MailAddress address,
246: String targetString) {
247: String result = null;
248:
249: try {
250: int msgPos = targetString.indexOf(':',
251: "regex:".length() + 1);
252:
253: // log("regex: targetString = " + targetString);
254: // log("regex: msgPos = " + msgPos);
255: // log("regex: compile " + targetString.substring("regex:".length(), msgPos));
256: // log("regex: address = " + address.toString());
257: // log("regex: replace = " + targetString.substring(msgPos + 1));
258:
259: Pattern pattern = new Perl5Compiler().compile(targetString
260: .substring("regex:".length(), msgPos));
261: Perl5Matcher matcher = new Perl5Matcher();
262:
263: if (matcher.matches(address.toString(), pattern)) {
264: MatchResult match = matcher.getMatch();
265: Map parameters = new HashMap(match.groups());
266: for (int i = 1; i < match.groups(); i++) {
267: parameters.put(Integer.toString(i), match.group(i));
268: }
269: result = XMLResources.replaceParameters(targetString
270: .substring(msgPos + 1), parameters);
271: }
272: } catch (Exception e) {
273: log("Exception during regexMap processing: ", e);
274: }
275:
276: // log("regex: result = " + result);
277: return result;
278: }
279:
280: /**
281: * Returns the character used to delineate multiple addresses.
282: *
283: * @param targetString the string to parse
284: * @return the character to tokenize on
285: */
286: private String getSeparator(String targetString) {
287: return (targetString.indexOf(',') > -1 ? "," : (targetString
288: .indexOf(';') > -1 ? ";" : (targetString
289: .indexOf("regex:") > -1 ? "" : ":")));
290: }
291:
292: private static final java.util.Random random = new java.util.Random(); // Used to generate new mail names
293:
294: /**
295: * Create a unique new primary key name.
296: *
297: * @param mail the mail to use as the basis for the new mail name
298: * @return a new name
299: */
300: private String newName(Mail mail) throws MessagingException {
301: String oldName = mail.getName();
302:
303: // Checking if the original mail name is too long, perhaps because of a
304: // loop caused by a configuration error.
305: // it could cause a "null pointer exception" in AvalonMailRepository much
306: // harder to understand.
307: if (oldName.length() > 76) {
308: int count = 0;
309: int index = 0;
310: while ((index = oldName.indexOf('!', index + 1)) >= 0) {
311: count++;
312: }
313: // It looks like a configuration loop. It's better to stop.
314: if (count > 7) {
315: throw new MessagingException(
316: "Unable to create a new message name: too long. Possible loop in config.xml.");
317: } else {
318: oldName = oldName.substring(0, 76);
319: }
320: }
321:
322: StringBuffer nameBuffer = new StringBuffer(64).append(oldName)
323: .append("-!").append(random.nextInt(1048576));
324: return nameBuffer.toString();
325: }
326: }
|