001    /****************************************************************
002     * Licensed to the Apache Software Foundation (ASF) under one   *
003     * or more contributor license agreements.  See the NOTICE file *
004     * distributed with this work for additional information        *
005     * regarding copyright ownership.  The ASF licenses this file   *
006     * to you under the Apache License, Version 2.0 (the            *
007     * "License"); you may not use this file except in compliance   *
008     * with the License.  You may obtain a copy of the License at   *
009     *                                                              *
010     *   http://www.apache.org/licenses/LICENSE-2.0                 *
011     *                                                              *
012     * Unless required by applicable law or agreed to in writing,   *
013     * software distributed under the License is distributed on an  *
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015     * KIND, either express or implied.  See the License for the    *
016     * specific language governing permissions and limitations      *
017     * under the License.                                           *
018     ****************************************************************/
019    
020    
021    package org.apache.james.jspf.terms;
022    
023    import org.apache.james.jspf.core.DNSLookupContinuation;
024    import org.apache.james.jspf.core.DNSRequest;
025    import org.apache.james.jspf.core.DNSResponse;
026    import org.apache.james.jspf.core.MacroExpand;
027    import org.apache.james.jspf.core.MacroExpandEnabled;
028    import org.apache.james.jspf.core.SPF1Constants;
029    import org.apache.james.jspf.core.SPFChecker;
030    import org.apache.james.jspf.core.SPFCheckerDNSResponseListener;
031    import org.apache.james.jspf.core.SPFSession;
032    import org.apache.james.jspf.core.SPFTermsRegexps;
033    import org.apache.james.jspf.core.exceptions.NeutralException;
034    import org.apache.james.jspf.core.exceptions.NoneException;
035    import org.apache.james.jspf.core.exceptions.PermErrorException;
036    import org.apache.james.jspf.core.exceptions.TempErrorException;
037    import org.apache.james.jspf.core.exceptions.TimeoutException;
038    
039    import java.util.List;
040    
041    /**
042     * This class represent the exp modifier
043     * 
044     */
045    public class ExpModifier extends GenericModifier implements MacroExpandEnabled, SPFCheckerDNSResponseListener {
046    
047        private final class ExpandedExplanationChecker implements SPFChecker {
048           
049            /**
050             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
051             */
052            public DNSLookupContinuation checkSPF(SPFSession spfData)
053                    throws PermErrorException, NoneException,
054                    TempErrorException, NeutralException {
055                try {
056                    String exp = (String) spfData.getAttribute(ATTRIBUTE_EXPAND_EXPLANATION);
057                    String expandedExplanation = macroExpand.expand(exp, spfData, MacroExpand.EXPLANATION);
058                    spfData.setExplanation(expandedExplanation);
059                } catch (PermErrorException e) {
060                    // ignore syntax error on explanation expansion
061                }
062                return null;
063            }
064        }
065    
066    
067        private final class ExpandedChecker implements SPFChecker {
068            
069            /**
070             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
071             */
072            public DNSLookupContinuation checkSPF(SPFSession spfData) throws PermErrorException,
073                    NoneException, TempErrorException, NeutralException {
074                String host = macroExpand.expand(getHost(), spfData, MacroExpand.DOMAIN);
075    
076                return new DNSLookupContinuation(new DNSRequest(host, DNSRequest.TXT), ExpModifier.this);
077            }
078        }
079    
080    
081        private static final String ATTRIBUTE_EXPAND_EXPLANATION = "ExpModifier.ExpandExplanation";
082    
083        /**
084         * ABNF: explanation = "exp" "=" domain-spec
085         * 
086         * NOTE: the last +"?" has been added to support RFC4408 ERRATA for the EXP modifier.
087         * An "exp=" should not result in a perm error but should be ignored.
088         * Errata: http://www.openspf.org/RFC_4408/Errata#empty-exp
089         * 
090         * NOTE: the last +"?" has been then removed because OpenSPF released a new testsuite
091         * requiring a PermError on "exp=" (see JSPF-56).
092         */
093        public static final String REGEX = "[eE][xX][pP]" + "\\="
094                + SPFTermsRegexps.DOMAIN_SPEC_REGEX;
095    
096        private MacroExpand macroExpand;
097    
098        private ExpandedChecker expandedChecker = new ExpandedChecker();
099    
100        private ExpandedExplanationChecker expandedExplanationChecker = new ExpandedExplanationChecker();
101    
102        /**
103         * Generate the explanation and set it in SPF1Data so it can be accessed
104         * easy later if needed
105         * 
106         * @param spfData
107         *            The SPF1Data which should used
108         * @throws PermErrorException 
109         * @throws TempErrorException 
110         * @throws NoneException 
111         * @throws NeutralException 
112         */
113        protected DNSLookupContinuation checkSPFLogged(SPFSession spfData) throws PermErrorException, TempErrorException, NeutralException, NoneException {
114            String host = getHost();
115            
116            // RFC4408 Errata: http://www.openspf.org/RFC_4408/Errata#empty-exp
117            if (host == null) {
118                return null;
119            }
120    
121            // If we should ignore the explanation we don't have to run this class
122            if (spfData.ignoreExplanation() == true)
123                return null;
124            
125            // If the currentResult is not fail we have no need to run all these
126            // methods!
127            if (spfData.getCurrentResult()== null || !spfData.getCurrentResult().equals(SPF1Constants.FAIL))
128                return null;
129    
130            spfData.pushChecker(expandedChecker);
131            return macroExpand.checkExpand(host, spfData, MacroExpand.DOMAIN);
132        }
133    
134        /**
135         * Get TXT records as a string
136         * 
137         * @param dns The DNSService to query
138         * @param strServer
139         *            The hostname for which we want to retrieve the TXT-Record
140         * @return String which reflect the TXT-Record
141         * @throws PermErrorException
142         *             if more then one TXT-Record for explanation was found
143         * @throws NoneException 
144         * @throws NeutralException 
145         * @throws TempErrorException 
146         * @throws TempErrorException
147         *             if the lookup result was "TRY_AGAIN"
148         */
149        
150        /**
151         * @see org.apache.james.jspf.core.SPFCheckerDNSResponseListener#onDNSResponse(org.apache.james.jspf.core.DNSResponse, org.apache.james.jspf.core.SPFSession)
152         */
153        public DNSLookupContinuation onDNSResponse(DNSResponse lookup, SPFSession spfData) throws PermErrorException, TempErrorException, NeutralException, NoneException {
154            try {
155                List<String> records = lookup.getResponse();
156            
157                if (records == null) {
158                    return null;
159                }
160        
161                // See SPF-Spec 6.2
162                //
163                // If domain-spec is empty, or there are any DNS processing errors (any RCODE other than 0), 
164                // or if no records are returned, or if more than one record is returned, or if there are syntax 
165                // errors in the explanation string, then proceed as if no exp modifier was given.   
166                if (records.size() > 1) {
167                    
168                    log.debug("More then one TXT-Record found for explanation");
169                    // Only catch the error and return null
170                    
171                } else {
172                    
173                    String exp = records.get(0);
174                    if (exp.length()>=2 && exp.charAt(0) == '"' && exp.charAt(exp.length() -1 ) == '"') {
175                        exp = exp.substring(1, exp.length() - 1);
176                    }
177    
178                    spfData.setAttribute(ATTRIBUTE_EXPAND_EXPLANATION, exp);
179                    
180                    if ((exp != null) && (!exp.equals(""))) {
181                        
182                        try {
183                            spfData.pushChecker(expandedExplanationChecker);
184                            return macroExpand.checkExpand(exp, spfData, MacroExpand.EXPLANATION);
185                        } catch (PermErrorException e) {
186                            // ignore syntax error on explanation expansion
187                        }
188                    }
189                    
190                }
191                
192    
193            } catch (TimeoutException e) {
194                // Nothing todo here.. just return null
195            }
196            return null;
197        }
198        
199        /**
200         * @see java.lang.Object#toString()
201         */
202        public String toString() {
203           return "exp="+getHost();
204        }
205    
206        /**
207         * @see org.apache.james.jspf.core.MacroExpandEnabled#enableMacroExpand(org.apache.james.jspf.core.MacroExpand)
208         */
209        public void enableMacroExpand(MacroExpand macroExpand) {
210            this.macroExpand = macroExpand;
211        }
212    
213    }