Package org.apache.james.jspf.core

Source Code of org.apache.james.jspf.core.MacroExpand$PTRResponseListener

/****************************************************************
* 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.jspf.core;

/**
* This Class is used to convert all macros which can used in SPF-Records to the
* right values!
*
*/

import org.apache.james.jspf.core.exceptions.NeutralException;
import org.apache.james.jspf.core.exceptions.NoneException;
import org.apache.james.jspf.core.exceptions.PermErrorException;
import org.apache.james.jspf.core.exceptions.TempErrorException;
import org.apache.james.jspf.core.exceptions.TimeoutException;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MacroExpand {

    private Pattern domainSpecPattern;

    private Pattern macroStringPattern;

    private Pattern macroLettersPattern;

    private Pattern macroLettersExpPattern;

    private Pattern cellPattern;

    private Logger log;

    private DNSService dnsProbe;

    public static final boolean EXPLANATION = true;
   
    public static final boolean DOMAIN = false;
   
    public static class RequireClientDomainException extends Exception {

        private static final long serialVersionUID = 3834282981657676530L;
       
    }

    /**
     * Construct MacroExpand
     *
     * @param logger the logget to use
     * @param dnsProbe the dns service to use
     */
    public MacroExpand(Logger logger, DNSService dnsProbe) {
        // This matches 2 groups
        domainSpecPattern = Pattern.compile(SPFTermsRegexps.DOMAIN_SPEC_REGEX_R);
        // The real pattern replacer
        macroStringPattern = Pattern.compile(SPFTermsRegexps.MACRO_STRING_REGEX_TOKEN);
        // The macro letters pattern
        macroLettersExpPattern = Pattern.compile(SPFTermsRegexps.MACRO_LETTER_PATTERN_EXP);
        macroLettersPattern = Pattern.compile(SPFTermsRegexps.MACRO_LETTER_PATTERN);
        log = logger;
        this.dnsProbe = dnsProbe;
    }
   

    private static final class AResponseListener implements
            SPFCheckerDNSResponseListener {
       
        /**
         * @see org.apache.james.jspf.core.SPFCheckerDNSResponseListener#onDNSResponse(org.apache.james.jspf.core.DNSResponse, org.apache.james.jspf.core.SPFSession)
         */
        public DNSLookupContinuation onDNSResponse(DNSResponse response, SPFSession session)
                throws PermErrorException, NoneException, TempErrorException,
                NeutralException {
            // just return the default "unknown" if we cannot find anything
            // later
            session.setClientDomain("unknown");
            try {
                List<String> records = response.getResponse();
                if (records != null && records.size() > 0) {
                    Iterator<String> i = records.iterator();
                    while (i.hasNext()) {
                        String next = i.next();
                        if (IPAddr.getAddress(session.getIpAddress())
                                .toString().equals(
                                        IPAddr.getAddress(next).toString())) {
                            session
                                    .setClientDomain((String) session
                                            .getAttribute(ATTRIBUTE_MACRO_EXPAND_CHECKED_RECORD));
                            break;
                        }
                    }
                }
            } catch (TimeoutException e) {
                // just return the default "unknown".
            } catch (PermErrorException e) {
                // just return the default "unknown".
            }
            return null;
        }
    }

    private static final class PTRResponseListener implements
            SPFCheckerDNSResponseListener {

        /**
         * @see org.apache.james.jspf.core.SPFCheckerDNSResponseListener#onDNSResponse(org.apache.james.jspf.core.DNSResponse, org.apache.james.jspf.core.SPFSession)
         */
        public DNSLookupContinuation onDNSResponse(DNSResponse response, SPFSession session)
                throws PermErrorException, NoneException, TempErrorException,
                NeutralException {

            try {
                boolean ip6 = IPAddr.isIPV6(session.getIpAddress());
                List<String> records = response.getResponse();

                if (records != null && records.size() > 0) {
                    String record = records.get(0);
                    session.setAttribute(ATTRIBUTE_MACRO_EXPAND_CHECKED_RECORD,
                            record);

                    return new DNSLookupContinuation(new DNSRequest(record,
                            ip6 ? DNSRequest.AAAA : DNSRequest.A),
                            new AResponseListener());

                }
            } catch (TimeoutException e) {
                // just return the default "unknown".
                session.setClientDomain("unknown");
            }
            return null;

        }
    }

    private static final String ATTRIBUTE_MACRO_EXPAND_CHECKED_RECORD = "MacroExpand.checkedRecord";

    public DNSLookupContinuation checkExpand(String input, SPFSession session, boolean isExplanation) throws PermErrorException, NoneException {
        if (input != null) {
            String host = this.expand(input, session, isExplanation);
            if (host == null) {

                return new DNSLookupContinuation(new DNSRequest(IPAddr
                        .getAddress(session.getIpAddress()).getReverseIP(),
                        DNSRequest.PTR), new PTRResponseListener());
            }
        }
        return null;
    }
   
    public String expand(String input, MacroData macroData, boolean isExplanation) throws PermErrorException {
        try {
            if (isExplanation) {
                return expandExplanation(input, macroData);
            } else {
                return expandDomain(input, macroData);
            }
        } catch (RequireClientDomainException e) {
            return null;
        }
    }

    /**
     * This method expand the given a explanation
     *
     * @param input
     *            The explanation which should be expanded
     * @return expanded The expanded explanation
     * @throws PermErrorException
     *             Get thrown if invalid macros are used
     * @throws RequireClientDomain
     */
    private String expandExplanation(String input, MacroData macroData) throws PermErrorException, RequireClientDomainException {

        log.debug("Start do expand explanation: " + input);

        String[] parts = input.split(" ");
        StringBuffer res = new StringBuffer();
        for (int i = 0; i < parts.length; i++) {
            if (i > 0) res.append(" ");
            res.append(expandMacroString(parts[i], macroData, true));
        }
        log.debug("Done expand explanation: " + res);
       
        return res.toString();
    }

    /**
     * This method expand the given domain. So all known macros get replaced
     *
     * @param input
     *            The domain which should be expand
     * @return expanded The domain with replaced macros
     * @throws PermErrorException
     *             This get thrown if invalid macros are used
     * @throws RequireClientDomain
     */
    private String expandDomain(String input, MacroData macroData) throws PermErrorException, RequireClientDomainException {

        log.debug("Start expand domain: " + input);

        Matcher inputMatcher = domainSpecPattern.matcher(input);
        if (!inputMatcher.matches() || inputMatcher.groupCount() != 2) {
            throw new PermErrorException("Invalid DomainSpec: "+input);
        }

        StringBuffer res = new StringBuffer();
        if (inputMatcher.group(1) != null && inputMatcher.group(1).length() > 0) {
            res.append(expandMacroString(inputMatcher.group(1), macroData, false));
        }
        if (inputMatcher.group(2) != null && inputMatcher.group(2).length() > 0) {
            if (inputMatcher.group(2).startsWith(".")) {
                res.append(inputMatcher.group(2));
            } else {
                res.append(expandMacroString(inputMatcher.group(2), macroData, false));
            }
        }
       
        String domainName = expandMacroString(input, macroData, false);
        // reduce to less than 255 characters, deleting subdomains from left
        int split = 0;
        while (domainName.length() > 255 && split > -1) {
            split = domainName.indexOf(".");
            domainName = domainName.substring(split + 1);
        }

        log.debug("Domain expanded: " + domainName);
       
        return domainName;
    }

    /**
     * Expand the given String
     *
     * @param input
     *            The inputString which should get expanded
     * @return expanded The expanded given String
     * @throws PermErrorException
     *             This get thrown if invalid macros are used
     * @throws RequireClientDomain
     */
    private String expandMacroString(String input, MacroData macroData, boolean isExplanation) throws PermErrorException, RequireClientDomainException {

        StringBuffer decodedValue = new StringBuffer();
        Matcher inputMatcher = macroStringPattern.matcher(input);
        String macroCell;
        int pos = 0;

        while (inputMatcher.find()) {
            String match2 = inputMatcher.group();
            if (pos != inputMatcher.start()) {
                throw new PermErrorException("Middle part does not match: "+input.substring(0,pos)+">>"+input.substring(pos, inputMatcher.start())+"<<"+input.substring(inputMatcher.start())+" ["+input+"]");
            }
            if (match2.length() > 0) {
                if (match2.startsWith("%{")) {
                    macroCell = input.substring(inputMatcher.start() + 2, inputMatcher
                            .end() - 1);
                    inputMatcher
                            .appendReplacement(decodedValue, escapeForMatcher(replaceCell(macroCell, macroData, isExplanation)));
                } else if (match2.length() == 2 && match2.startsWith("%")) {
                    // handle the % escaping
                    /*
                     * From RFC4408:
                     *
                     * A literal "%" is expressed by "%%".
                     *   "%_" expands to a single " " space.
                     *   "%-" expands to a URL-encoded space, viz., "%20".
                     */
                    if ("%_".equals(match2)) {
                        inputMatcher.appendReplacement(decodedValue, " ");
                    } else if ("%-".equals(match2)) {
                        inputMatcher.appendReplacement(decodedValue, "%20");
                    } else {
                        inputMatcher.appendReplacement(decodedValue, escapeForMatcher(match2.substring(1)));
                    }
                }
            }
           
            pos = inputMatcher.end();
        }
       
        if (input.length() != pos) {
            throw new PermErrorException("End part does not match: "+input.substring(pos));
        }
       
        inputMatcher.appendTail(decodedValue);

        return decodedValue.toString();
    }

    /**
     * Replace the macros in given String
     *
     * @param replaceValue
     *            The String in which known macros should get replaced
     * @return returnData The String with replaced macros
     * @throws PermErrorException
     *             Get thrown if an error in processing happen
     * @throws RequireClientDomain
     */
    private String replaceCell(String replaceValue, MacroData macroData, boolean isExplanation) throws PermErrorException, RequireClientDomainException {

        String variable = "";
        String domainNumber = "";
        boolean isReversed = false;
        String delimeters = ".";

       
        // Get only command character so that 'r' command and 'r' modifier don't
        // clash
        String commandCharacter = replaceValue.substring(0, 1);
        Matcher cellMatcher;
        // Find command
        if (isExplanation) {
            cellMatcher = macroLettersExpPattern.matcher(commandCharacter);
        } else {
            cellMatcher = macroLettersPattern.matcher(commandCharacter);
        }
        if (cellMatcher.find()) {
            if (cellMatcher.group().toUpperCase().equals(cellMatcher.group())) {
                variable = encodeURL(matchMacro(cellMatcher.group(), macroData));
            } else {
                variable = matchMacro(cellMatcher.group(), macroData);
            }
            // Remove Macro code so that r macro code does not clash with r the
            // reverse modifier
            replaceValue = replaceValue.substring(1);
        } else {
            throw new PermErrorException("MacroLetter not found: "+replaceValue);
        }

        // Find number of domains to use
        cellPattern = Pattern.compile("\\d+");
        cellMatcher = cellPattern.matcher(replaceValue);
        while (cellMatcher.find()) {
            domainNumber = cellMatcher.group();
            if (Integer.parseInt(domainNumber) == 0) {
                throw new PermErrorException(
                        "Digit transformer must be non-zero");
            }
        }
        // find if reversed
        cellPattern = Pattern.compile("r");
        cellMatcher = cellPattern.matcher(replaceValue);
        while (cellMatcher.find()) {
            isReversed = true;
        }

        // find delimeters
        cellPattern = Pattern.compile("[\\.\\-\\+\\,\\/\\_\\=]+");
        cellMatcher = cellPattern.matcher(replaceValue);
        while (cellMatcher.find()) {
            delimeters = cellMatcher.group();
        }

        // Reverse domains as necessary
        ArrayList<String> data = split(variable, delimeters);
        if (isReversed) {
            data = reverse(data);
        }

        // Truncate domain name to number of sub sections
        String returnData;
        if (!domainNumber.equals("")) {
            returnData = subset(data, Integer.parseInt(domainNumber));
        } else {
            returnData = subset(data);
        }

        return returnData;

    }

    /**
     * Get the value for the given macro like descripted in the RFC
     *
     * @param macro
     *            The macro we want to get the value for
     * @return rValue The value for the given macro
     * @throws PermErrorException
     *             Get thrown if the given variable is an unknown macro
     * @throws RequireClientDomain requireClientDomain if the client domain is needed
     *             and not yet resolved.
     */
    private String matchMacro(String macro, MacroData macroData) throws PermErrorException, RequireClientDomainException {

        String rValue = null;

        String variable = macro.toLowerCase();
        if (variable.equalsIgnoreCase("i")) {
            rValue = macroData.getMacroIpAddress();
        } else if (variable.equalsIgnoreCase("s")) {
            rValue = macroData.getMailFrom();
        } else if (variable.equalsIgnoreCase("h")) {
            rValue = macroData.getHostName();
        } else if (variable.equalsIgnoreCase("l")) {
            rValue = macroData.getCurrentSenderPart();
        } else if (variable.equalsIgnoreCase("d")) {
            rValue = macroData.getCurrentDomain();
        } else if (variable.equalsIgnoreCase("v")) {
            rValue = macroData.getInAddress();
        } else if (variable.equalsIgnoreCase("t")) {
            rValue = Long.toString(macroData.getTimeStamp());
        } else if (variable.equalsIgnoreCase("c")) {
            rValue = macroData.getReadableIP();
        } else if (variable.equalsIgnoreCase("p")) {
            rValue = macroData.getClientDomain();
            if (rValue == null) {
                throw new RequireClientDomainException();
            }
        } else if (variable.equalsIgnoreCase("o")) {
            rValue = macroData.getSenderDomain();
        } else if (variable.equalsIgnoreCase("r")) {
            rValue = macroData.getReceivingDomain();
            if (rValue == null) {
                rValue = "unknown";
                List<String> dNames = dnsProbe.getLocalDomainNames();

                for (int i = 0; i < dNames.size(); i++) {
                    // check if the domainname is a FQDN
                    if (SPF1Utils.checkFQDN(dNames.get(i).toString())) {
                        rValue = dNames.get(i).toString();
                        if (macroData instanceof SPFSession) {
                            ((SPFSession) macroData).setReceivingDomain(rValue);
                        }
                        break;
                    }
                }
            }
        }

        if (rValue == null) {
            throw new PermErrorException("Unknown command : " + variable);

        } else {
            log.debug("Used macro: " + macro + " replaced with: " + rValue);

            return rValue;
        }
    }

    /**
     * Create an ArrayList by the given String. The String get splitted by given
     * delimeters and one entry in the Array will be made for each splited
     * String
     *
     * @param data
     *            The String we want to put in the Array
     * @param delimeters
     *            The delimeter we want to use to split the String
     * @return ArrayList which contains the String parts
     */
    private ArrayList<String> split(String data, String delimeters) {

        String currentChar;
        StringBuffer element = new StringBuffer();
        ArrayList<String> splitParts = new ArrayList<String>();

        for (int i = 0; i < data.length(); i++) {
            currentChar = data.substring(i, i + 1);
            if (delimeters.indexOf(currentChar) > -1) {
                splitParts.add(element.toString());
                element.setLength(0);
            } else {
                element.append(currentChar);
            }
        }
        splitParts.add(element.toString());
        return splitParts;
    }

    /**
     * Reverse an ArrayList
     *
     * @param data
     *            The ArrayList we want to get reversed
     * @return reversed The reversed given ArrayList
     */
    private ArrayList<String> reverse(ArrayList<String> data) {

        ArrayList<String> reversed = new ArrayList<String>();
        for (int i = 0; i < data.size(); i++) {
            reversed.add(0, data.get(i));
        }
        return reversed;
    }

    /**
     * @see #subset(ArrayList, int)
     */
    private String subset(ArrayList<String> data) {
        return subset(data, data.size());
    }

    /**
     * Convert a ArrayList to a String which holds the entries seperated by dots
     *
     * @param data The ArrayList which should be converted
     * @param length The ArrayLength
     * @return A String which holds all entries seperated by dots
     */
    private String subset(ArrayList<String> data, int length) {

        StringBuffer buildString = new StringBuffer();
        if (data.size() < length) {
            length = data.size();
        }
        int start = data.size() - length;
        for (int i = start; i < data.size(); i++) {
            if (buildString.length() > 0) {
                buildString.append(".");
            }
            buildString.append(data.get(i));
        }
        return buildString.toString();

    }

    /**
     * Encode the given URL to UTF-8
     *
     * @param data
     *            url to encode
     * @return encoded URL
     */
    private String encodeURL(String data) {

        try {
            // TODO URLEncoder method is not RFC2396 compatible, known
            // difference
            // is Space character gets converted to "+" rather than "%20"
            // Is there anything else which is not correct with URLEncoder?
            // Couldn't find a RFC2396 encoder
            data = URLEncoder.encode(data, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // This shouldn't happen ignore it!
        }

        // workaround for the above descripted problem
        return data.replaceAll("\\+", "%20");

    }
   
    /**
     * Because Dollar signs may be treated as references to captured subsequences in method Matcher.appendReplacement
     * its necessary to escape Dollar signs because its allowed in the local-part of an emailaddress.
     *
     * See JSPF-71 for the bugreport
     *
     * @param raw
     * @return escaped string
     */
    private String escapeForMatcher(String raw) {
        StringBuffer sb = new StringBuffer();

        for (int i = 0; i < raw.length(); i++) {
            char c = raw.charAt(i);
            if (c == '$' || c == '\\') {
                sb.append('\\');
            }
            sb.append(c);
        }
        return sb.toString();
    }

}
TOP

Related Classes of org.apache.james.jspf.core.MacroExpand$PTRResponseListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.