FileDocCategorySizeDatePackage
AbstractVirtualUserTable.javaAPI DocApache James 2.3.113528Fri Jan 12 12:56:28 GMT 2007org.apache.james.transport.mailets

AbstractVirtualUserTable.java

/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.transport.mailets;

import org.apache.james.core.MailImpl;
import org.apache.james.util.XMLResources;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;

import javax.mail.MessagingException;
import javax.mail.internet.ParseException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * Provides an abstraction of common functionality needed for implementing
 * a Virtual User Table. Override the <code>mapRecipients</code> method to
 * map virtual recipients to real recipients.
 */
public abstract class AbstractVirtualUserTable extends GenericMailet
{
    static private final String MARKER = "org.apache.james.transport.mailets.AbstractVirtualUserTable.mapped";

    /**
     * Checks the recipient list of the email for user mappings.  Maps recipients as
     * appropriate, modifying the recipient list of the mail and sends mail to any new
     * non-local recipients.
     *
     * @param mail the mail to process
     */
    public void service(Mail mail) throws MessagingException
    {
        if (mail.getAttribute(MARKER) != null) {
            mail.removeAttribute(MARKER);
            return;
        }

        Collection recipientsToRemove = new HashSet();
        Collection recipientsToAddLocal = new ArrayList();
        Collection recipientsToAddForward = new ArrayList();

        Collection recipients = mail.getRecipients();
        Map recipientsMap = new HashMap(recipients.size());

        for (Iterator iter = recipients.iterator(); iter.hasNext(); ) {
            MailAddress address = (MailAddress)iter.next();

            // Assume all addresses are non-virtual at start
            recipientsMap.put(address, null);
        }

        mapRecipients(recipientsMap);

        for (Iterator iter = recipientsMap.keySet().iterator(); iter.hasNext(); ) {
            MailAddress source = (MailAddress)iter.next();
            String targetString = (String)recipientsMap.get(source);

            // Only non-null mappings are translated
            if(targetString != null) {
                if (targetString.startsWith("error:")) {
                    //Mark this source address as an address to remove from the recipient list
                    recipientsToRemove.add(source);
                    processDSN(mail, source, targetString);
                } else {
                    StringTokenizer tokenizer = new StringTokenizer(targetString, getSeparator(targetString));

                    while (tokenizer.hasMoreTokens()) {
                        String targetAddress = tokenizer.nextToken().trim();

                        // log("Attempting to map from " + source + " to " + targetAddress);

                        if (targetAddress.startsWith("regex:")) {
                            targetAddress = regexMap(mail, source, targetAddress);
                            if (targetAddress == null) continue;
                        }

                        try {
                            MailAddress target = (targetAddress.indexOf('@') < 0) ? new MailAddress(targetAddress, "localhost")
                                : new MailAddress(targetAddress);

                            //Mark this source address as an address to remove from the recipient list
                            recipientsToRemove.add(source);

                            // We need to separate local and remote
                            // recipients.  This is explained below.
                            if (getMailetContext().isLocalServer(target.getHost())) {
                                recipientsToAddLocal.add(target);
                            } else {
                                recipientsToAddForward.add(target);
                            }

                            StringBuffer buf = new StringBuffer().append("Translating virtual user ")
                                                                 .append(source)
                                                                 .append(" to ")
                                                                 .append(target);
                            log(buf.toString());

                        } catch (ParseException pe) {
                            //Don't map this address... there's an invalid address mapping here
                            StringBuffer exceptionBuffer =
                                new StringBuffer(128)
                                .append("There is an invalid map from ")
                                .append(source)
                                .append(" to ")
                                .append(targetAddress);
                            log(exceptionBuffer.toString());
                            continue;
                        }
                    }
                }
            }
        }

        // Remove mapped recipients
        recipients.removeAll(recipientsToRemove);

        // Add mapped recipients that are local
        recipients.addAll(recipientsToAddLocal);

        // We consider an address that we map to be, by definition, a
        // local address.  Therefore if we mapped to a remote address,
        // then we want to make sure that the mail can be relayed.
        // However, the original e-mail would typically be subjected to
        // relay testing.  By posting a new mail back through the
        // system, we have a locally generated mail, which will not be
        // subjected to relay testing.

        // Forward to mapped recipients that are remote
        if (recipientsToAddForward.size() != 0) {
            // Can't use this ... some mappings could lead to an infinite loop
            // getMailetContext().sendMail(mail.getSender(), recipientsToAddForward, mail.getMessage());

            // duplicates the Mail object, to be able to modify the new mail keeping the original untouched
            MailImpl newMail = new MailImpl(mail,newName(mail));
            try {
                try {
                    newMail.setRemoteAddr(java.net.InetAddress.getLocalHost().getHostAddress());
                    newMail.setRemoteHost(java.net.InetAddress.getLocalHost().getHostName());
                } catch (java.net.UnknownHostException _) {
                    newMail.setRemoteAddr("127.0.0.1");
                    newMail.setRemoteHost("localhost");
                }
                newMail.setRecipients(recipientsToAddForward);
                newMail.setAttribute(MARKER, Boolean.TRUE);
                getMailetContext().sendMail(newMail);
            } finally {
                newMail.dispose();
            }
        }

        // If there are no recipients left, Ghost the message
        if (recipients.size() == 0) {
            mail.setState(Mail.GHOST);
        }
    }

    /**
     * Override to map virtual recipients to real recipients, both local and non-local.
     * Each key in the provided map corresponds to a potential virtual recipient, stored as
     * a <code>MailAddress</code> object.
     * 
     * Translate virtual recipients to real recipients by mapping a string containing the
     * address of the real recipient as a value to a key. Leave the value <code>null<code>
     * if no mapping should be performed. Multiple recipients may be specified by delineating
     * the mapped string with commas, semi-colons or colons.
     * 
     * @param recipientsMap the mapping of virtual to real recipients, as 
     *    <code>MailAddress</code>es to <code>String</code>s.
     */
    protected abstract void mapRecipients(Map recipientsMap) throws MessagingException;
  
    /**
     * Sends the message for DSN processing
     *
     * @param mail the Mail instance being processed
     * @param address the MailAddress causing the DSN
     * @param error a String in the form "error:<code> <msg>"
     */
    private void processDSN(Mail mail, MailAddress address, String error) {
        // parse "error:<code> <msg>"
      int msgPos = error.indexOf(' ');
      try {
          Integer code = Integer.valueOf(error.substring("error:".length(),msgPos));
      } catch (NumberFormatException e) {
          log("Cannot send DSN.  Exception parsing DSN code from: " + error, e);
          return;
      }
      String msg = error.substring(msgPos + 1);
      // process bounce for "source" address
      try {
          getMailetContext().bounce(mail, error);
      }
      catch (MessagingException me) {
          log("Cannot send DSN.  Exception during DSN processing: ", me);
      }
  }

  /**
   * Processes regex virtual user mapping
   *
   * If a mapped target string begins with the prefix regex:, it must be
   * formatted as regex:<regular-expression>:<parameterized-string>,
   * e.g., regex:(.*)@(.*):${1}@tld
   *
   * @param mail the Mail instance being processed
   * @param address the MailAddress to be mapped
   * @param targetString a String specifying the mapping
   */
  private String regexMap(Mail mail, MailAddress address, String targetString) {
      String result = null;

      try {
          int msgPos = targetString.indexOf(':', "regex:".length() + 1);

          // log("regex: targetString = " + targetString);
          // log("regex: msgPos = " + msgPos);
          // log("regex: compile " + targetString.substring("regex:".length(), msgPos));
          // log("regex: address = " + address.toString());
          // log("regex: replace = " + targetString.substring(msgPos + 1));

          Pattern pattern = new Perl5Compiler().compile(targetString.substring("regex:".length(), msgPos));
          Perl5Matcher matcher = new Perl5Matcher();

          if (matcher.matches(address.toString(), pattern)) {
              MatchResult match = matcher.getMatch();
              Map parameters = new HashMap(match.groups());
              for (int i = 1; i < match.groups(); i++) {
                  parameters.put(Integer.toString(i), match.group(i));
              }
              result = XMLResources.replaceParameters(targetString.substring(msgPos + 1), parameters);
          }
      }
      catch (Exception e) {
          log("Exception during regexMap processing: ", e);
      }

      // log("regex: result = " + result);
      return result;
  }

  /**
   * Returns the character used to delineate multiple addresses.
   * 
   * @param targetString the string to parse
   * @return the character to tokenize on
   */
  private String getSeparator(String targetString) {
      return (targetString.indexOf(',') > -1 ? "," : (targetString.indexOf(';') > -1 ? ";" : (targetString.indexOf("regex:") > -1? "" : ":" )));
  }

  private static final java.util.Random random = new java.util.Random();  // Used to generate new mail names

  /**
   * Create a unique new primary key name.
   *
   * @param mail the mail to use as the basis for the new mail name
   * @return a new name
   */
  private String newName(Mail mail) throws MessagingException {
      String oldName = mail.getName();

        // Checking if the original mail name is too long, perhaps because of a
        // loop caused by a configuration error.
        // it could cause a "null pointer exception" in AvalonMailRepository much
        // harder to understand.
      if (oldName.length() > 76) {
          int count = 0;
          int index = 0;
          while ((index = oldName.indexOf('!', index + 1)) >= 0) {
              count++;
          }
            // It looks like a configuration loop. It's better to stop.
          if (count > 7) {
              throw new MessagingException("Unable to create a new message name: too long.  Possible loop in config.xml.");
          }
          else {
              oldName = oldName.substring(0, 76);
          }
      }

      StringBuffer nameBuffer =
                               new StringBuffer(64)
                               .append(oldName)
                               .append("-!")
                               .append(random.nextInt(1048576));
      return nameBuffer.toString();
  }
}