View Javadoc

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              // Assume all addresses are non-virtual at start
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              // Only non-null mappings are translated
87              if(targetString != null) {
88                  if (targetString.startsWith("error:")) {
89                      //Mark this source address as an address to remove from the recipient list
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                          // log("Attempting to map from " + source + " to " + targetAddress);
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                             //Mark this source address as an address to remove from the recipient list
110                             recipientsToRemove.add(source);
111 
112                             // We need to separate local and remote
113                             // recipients.  This is explained below.
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                             //Don't map this address... there's an invalid address mapping here
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         // Remove mapped recipients
143         recipients.removeAll(recipientsToRemove);
144 
145         // Add mapped recipients that are local
146         recipients.addAll(recipientsToAddLocal);
147 
148         // We consider an address that we map to be, by definition, a
149         // local address.  Therefore if we mapped to a remote address,
150         // then we want to make sure that the mail can be relayed.
151         // However, the original e-mail would typically be subjected to
152         // relay testing.  By posting a new mail back through the
153         // system, we have a locally generated mail, which will not be
154         // subjected to relay testing.
155 
156         // Forward to mapped recipients that are remote
157         if (recipientsToAddForward.size() != 0) {
158             // Can't use this ... some mappings could lead to an infinite loop
159             // getMailetContext().sendMail(mail.getSender(), recipientsToAddForward, mail.getMessage());
160 
161             // duplicates the Mail object, to be able to modify the new mail keeping the original untouched
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         // If there are no recipients left, Ghost the message
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         // parse "error:<code> <msg>"
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       // process bounce for "source" address
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           // log("regex: targetString = " + targetString);
244           // log("regex: msgPos = " + msgPos);
245           // log("regex: compile " + targetString.substring("regex:".length(), msgPos));
246           // log("regex: address = " + address.toString());
247           // log("regex: replace = " + targetString.substring(msgPos + 1));
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       // log("regex: result = " + result);
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();  // Used to generate new mail names
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         // Checking if the original mail name is too long, perhaps because of a
291         // loop caused by a configuration error.
292         // it could cause a "null pointer exception" in AvalonMailRepository much
293         // harder to understand.
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             // It looks like a configuration loop. It's better to stop.
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 }