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.james.core.MailImpl;
23 import org.apache.james.util.XMLResources;
24 import org.apache.mailet.GenericMailet;
25 import org.apache.mailet.Mail;
26 import org.apache.mailet.MailAddress;
27 import org.apache.oro.text.regex.MatchResult;
28 import org.apache.oro.text.regex.Pattern;
29 import org.apache.oro.text.regex.Perl5Compiler;
30 import org.apache.oro.text.regex.Perl5Matcher;
31
32 import javax.mail.MessagingException;
33 import javax.mail.internet.ParseException;
34
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.Iterator;
40 import java.util.Map;
41 import java.util.StringTokenizer;
42
43 /***
44 * Provides an abstraction of common functionality needed for implementing
45 * a Virtual User Table. Override the <code>mapRecipients</code> method to
46 * map virtual recipients to real recipients.
47 */
48 public abstract class AbstractVirtualUserTable extends GenericMailet
49 {
50 static private final String MARKER = "org.apache.james.transport.mailets.AbstractVirtualUserTable.mapped";
51
52 /***
53 * Checks the recipient list of the email for user mappings. Maps recipients as
54 * appropriate, modifying the recipient list of the mail and sends mail to any new
55 * non-local recipients.
56 *
57 * @param mail the mail to process
58 */
59 public void service(Mail mail) throws MessagingException
60 {
61 if (mail.getAttribute(MARKER) != null) {
62 mail.removeAttribute(MARKER);
63 return;
64 }
65
66 Collection recipientsToRemove = new HashSet();
67 Collection recipientsToAddLocal = new ArrayList();
68 Collection recipientsToAddForward = new ArrayList();
69
70 Collection recipients = mail.getRecipients();
71 Map recipientsMap = new HashMap(recipients.size());
72
73 for (Iterator iter = recipients.iterator(); iter.hasNext(); ) {
74 MailAddress address = (MailAddress)iter.next();
75
76
77 recipientsMap.put(address, null);
78 }
79
80 mapRecipients(recipientsMap);
81
82 for (Iterator iter = recipientsMap.keySet().iterator(); iter.hasNext(); ) {
83 MailAddress source = (MailAddress)iter.next();
84 String targetString = (String)recipientsMap.get(source);
85
86
87 if(targetString != null) {
88 if (targetString.startsWith("error:")) {
89
90 recipientsToRemove.add(source);
91 processDSN(mail, source, targetString);
92 } else {
93 StringTokenizer tokenizer = new StringTokenizer(targetString, getSeparator(targetString));
94
95 while (tokenizer.hasMoreTokens()) {
96 String targetAddress = tokenizer.nextToken().trim();
97
98
99
100 if (targetAddress.startsWith("regex:")) {
101 targetAddress = regexMap(mail, source, targetAddress);
102 if (targetAddress == null) continue;
103 }
104
105 try {
106 MailAddress target = (targetAddress.indexOf('@') < 0) ? new MailAddress(targetAddress, "localhost")
107 : new MailAddress(targetAddress);
108
109
110 recipientsToRemove.add(source);
111
112
113
114 if (getMailetContext().isLocalServer(target.getHost())) {
115 recipientsToAddLocal.add(target);
116 } else {
117 recipientsToAddForward.add(target);
118 }
119
120 StringBuffer buf = new StringBuffer().append("Translating virtual user ")
121 .append(source)
122 .append(" to ")
123 .append(target);
124 log(buf.toString());
125
126 } catch (ParseException pe) {
127
128 StringBuffer exceptionBuffer =
129 new StringBuffer(128)
130 .append("There is an invalid map from ")
131 .append(source)
132 .append(" to ")
133 .append(targetAddress);
134 log(exceptionBuffer.toString());
135 continue;
136 }
137 }
138 }
139 }
140 }
141
142
143 recipients.removeAll(recipientsToRemove);
144
145
146 recipients.addAll(recipientsToAddLocal);
147
148
149
150
151
152
153
154
155
156
157 if (recipientsToAddForward.size() != 0) {
158
159
160
161
162 MailImpl newMail = new MailImpl(mail,newName(mail));
163 try {
164 try {
165 newMail.setRemoteAddr(java.net.InetAddress.getLocalHost().getHostAddress());
166 newMail.setRemoteHost(java.net.InetAddress.getLocalHost().getHostName());
167 } catch (java.net.UnknownHostException _) {
168 newMail.setRemoteAddr("127.0.0.1");
169 newMail.setRemoteHost("localhost");
170 }
171 newMail.setRecipients(recipientsToAddForward);
172 newMail.setAttribute(MARKER, Boolean.TRUE);
173 getMailetContext().sendMail(newMail);
174 } finally {
175 newMail.dispose();
176 }
177 }
178
179
180 if (recipients.size() == 0) {
181 mail.setState(Mail.GHOST);
182 }
183 }
184
185 /***
186 * Override to map virtual recipients to real recipients, both local and non-local.
187 * Each key in the provided map corresponds to a potential virtual recipient, stored as
188 * a <code>MailAddress</code> object.
189 *
190 * Translate virtual recipients to real recipients by mapping a string containing the
191 * address of the real recipient as a value to a key. Leave the value <code>null<code>
192 * if no mapping should be performed. Multiple recipients may be specified by delineating
193 * the mapped string with commas, semi-colons or colons.
194 *
195 * @param recipientsMap the mapping of virtual to real recipients, as
196 * <code>MailAddress</code>es to <code>String</code>s.
197 */
198 protected abstract void mapRecipients(Map recipientsMap) throws MessagingException;
199
200 /***
201 * Sends the message for DSN processing
202 *
203 * @param mail the Mail instance being processed
204 * @param address the MailAddress causing the DSN
205 * @param error a String in the form "error:<code> <msg>"
206 */
207 private void processDSN(Mail mail, MailAddress address, String error) {
208
209 int msgPos = error.indexOf(' ');
210 try {
211 Integer code = Integer.valueOf(error.substring("error:".length(),msgPos));
212 } catch (NumberFormatException e) {
213 log("Cannot send DSN. Exception parsing DSN code from: " + error, e);
214 return;
215 }
216 String msg = error.substring(msgPos + 1);
217
218 try {
219 getMailetContext().bounce(mail, error);
220 }
221 catch (MessagingException me) {
222 log("Cannot send DSN. Exception during DSN processing: ", me);
223 }
224 }
225
226 /***
227 * Processes regex virtual user mapping
228 *
229 * If a mapped target string begins with the prefix regex:, it must be
230 * formatted as regex:<regular-expression>:<parameterized-string>,
231 * e.g., regex:(.*)@(.*):${1}@tld
232 *
233 * @param mail the Mail instance being processed
234 * @param address the MailAddress to be mapped
235 * @param targetString a String specifying the mapping
236 */
237 private String regexMap(Mail mail, MailAddress address, String targetString) {
238 String result = null;
239
240 try {
241 int msgPos = targetString.indexOf(':', "regex:".length() + 1);
242
243
244
245
246
247
248
249 Pattern pattern = new Perl5Compiler().compile(targetString.substring("regex:".length(), msgPos));
250 Perl5Matcher matcher = new Perl5Matcher();
251
252 if (matcher.matches(address.toString(), pattern)) {
253 MatchResult match = matcher.getMatch();
254 Map parameters = new HashMap(match.groups());
255 for (int i = 1; i < match.groups(); i++) {
256 parameters.put(Integer.toString(i), match.group(i));
257 }
258 result = XMLResources.replaceParameters(targetString.substring(msgPos + 1), parameters);
259 }
260 }
261 catch (Exception e) {
262 log("Exception during regexMap processing: ", e);
263 }
264
265
266 return result;
267 }
268
269 /***
270 * Returns the character used to delineate multiple addresses.
271 *
272 * @param targetString the string to parse
273 * @return the character to tokenize on
274 */
275 private String getSeparator(String targetString) {
276 return (targetString.indexOf(',') > -1 ? "," : (targetString.indexOf(';') > -1 ? ";" : (targetString.indexOf("regex:") > -1? "" : ":" )));
277 }
278
279 private static final java.util.Random random = new java.util.Random();
280
281 /***
282 * Create a unique new primary key name.
283 *
284 * @param mail the mail to use as the basis for the new mail name
285 * @return a new name
286 */
287 private String newName(Mail mail) throws MessagingException {
288 String oldName = mail.getName();
289
290
291
292
293
294 if (oldName.length() > 76) {
295 int count = 0;
296 int index = 0;
297 while ((index = oldName.indexOf('!', index + 1)) >= 0) {
298 count++;
299 }
300
301 if (count > 7) {
302 throw new MessagingException("Unable to create a new message name: too long. Possible loop in config.xml.");
303 }
304 else {
305 oldName = oldName.substring(0, 76);
306 }
307 }
308
309 StringBuffer nameBuffer =
310 new StringBuffer(64)
311 .append(oldName)
312 .append("-!")
313 .append(random.nextInt(1048576));
314 return nameBuffer.toString();
315 }
316 }