View Javadoc

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