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.impl;
022    
023    import org.apache.james.jspf.core.DNSLookupContinuation;
024    import org.apache.james.jspf.core.DNSService;
025    import org.apache.james.jspf.core.DNSServiceEnabled;
026    import org.apache.james.jspf.core.LogEnabled;
027    import org.apache.james.jspf.core.Logger;
028    import org.apache.james.jspf.core.MacroExpand;
029    import org.apache.james.jspf.core.MacroExpandEnabled;
030    import org.apache.james.jspf.core.SPF1Record;
031    import org.apache.james.jspf.core.SPF1Utils;
032    import org.apache.james.jspf.core.SPFCheckEnabled;
033    import org.apache.james.jspf.core.SPFChecker;
034    import org.apache.james.jspf.core.SPFCheckerExceptionCatcher;
035    import org.apache.james.jspf.core.SPFRecordParser;
036    import org.apache.james.jspf.core.SPFSession;
037    import org.apache.james.jspf.core.exceptions.NeutralException;
038    import org.apache.james.jspf.core.exceptions.NoneException;
039    import org.apache.james.jspf.core.exceptions.PermErrorException;
040    import org.apache.james.jspf.core.exceptions.SPFErrorConstants;
041    import org.apache.james.jspf.core.exceptions.SPFResultException;
042    import org.apache.james.jspf.core.exceptions.TempErrorException;
043    import org.apache.james.jspf.executor.FutureSPFResult;
044    import org.apache.james.jspf.executor.SPFExecutor;
045    import org.apache.james.jspf.executor.SPFResult;
046    import org.apache.james.jspf.executor.SynchronousSPFExecutor;
047    import org.apache.james.jspf.parser.RFC4408SPF1Parser;
048    import org.apache.james.jspf.policies.InitialChecksPolicy;
049    import org.apache.james.jspf.policies.NeutralIfNotMatchPolicy;
050    import org.apache.james.jspf.policies.NoSPFRecordFoundPolicy;
051    import org.apache.james.jspf.policies.ParseRecordPolicy;
052    import org.apache.james.jspf.policies.Policy;
053    import org.apache.james.jspf.policies.PolicyPostFilter;
054    import org.apache.james.jspf.policies.SPFRetriever;
055    import org.apache.james.jspf.policies.SPFStrictCheckerRetriever;
056    import org.apache.james.jspf.policies.local.BestGuessPolicy;
057    import org.apache.james.jspf.policies.local.DefaultExplanationPolicy;
058    import org.apache.james.jspf.policies.local.FallbackPolicy;
059    import org.apache.james.jspf.policies.local.OverridePolicy;
060    import org.apache.james.jspf.policies.local.TrustedForwarderPolicy;
061    import org.apache.james.jspf.wiring.WiringServiceTable;
062    
063    import java.util.Iterator;
064    import java.util.LinkedList;
065    
066    /**
067     * This class is used to generate a SPF-Test and provided all intressting data.
068     */
069    public class SPF implements SPFChecker {
070    
071        private static final class SPFRecordChecker implements SPFChecker {
072            
073            /**
074             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
075             */
076            public DNSLookupContinuation checkSPF(SPFSession spfData)
077                    throws PermErrorException, TempErrorException,
078                    NeutralException, NoneException {
079                
080                SPF1Record spfRecord = (SPF1Record) spfData.getAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD);
081                // make sure we cleanup the record, for recursion support
082                spfData.removeAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD);
083                
084                LinkedList<SPFChecker> policyCheckers = new LinkedList<SPFChecker>();
085                
086                Iterator<SPFChecker> i = spfRecord.iterator();
087                while (i.hasNext()) {
088                    SPFChecker checker = i.next();
089                    policyCheckers.add(checker);
090                }
091    
092                while (policyCheckers.size() > 0) {
093                    SPFChecker removeLast = policyCheckers.removeLast();
094                    spfData.pushChecker(removeLast);
095                }
096    
097                return null;
098            }
099        }
100    
101        private static final class PolicyChecker implements SPFChecker {
102            
103            private LinkedList<SPFChecker> policies;
104            
105            public PolicyChecker(LinkedList<SPFChecker> policies) {
106                this.policies = policies;
107            }
108            
109            /**
110             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
111             */
112            public DNSLookupContinuation checkSPF(SPFSession spfData)
113                    throws PermErrorException, TempErrorException,
114                    NeutralException, NoneException {
115                
116                while (policies.size() > 0) {
117                    SPFChecker removeLast = policies.removeLast();
118                    spfData.pushChecker(removeLast);
119                }
120                
121                return null;
122            }
123        }
124    
125        private static final class SPFPolicyChecker implements SPFChecker {
126            private Policy policy;
127    
128            /**
129             * @param policy
130             */
131            public SPFPolicyChecker(Policy policy) {
132                this.policy = policy;
133            }
134    
135            /**
136             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
137             */
138            public DNSLookupContinuation checkSPF(SPFSession spfData)
139                    throws PermErrorException, TempErrorException,
140                    NeutralException, NoneException {
141                SPF1Record res = (SPF1Record) spfData.getAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD);
142                if (res == null) {
143                    res = policy.getSPFRecord(spfData.getCurrentDomain());
144                    spfData.setAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD, res);
145                }
146                return null;
147            }
148            
149            public String toString() {
150                return "PC:"+policy.toString();
151            }
152        }
153    
154        private static final class SPFPolicyPostFilterChecker implements SPFChecker {
155            private PolicyPostFilter policy;
156    
157            /**
158             * @param policy
159             */
160            public SPFPolicyPostFilterChecker(PolicyPostFilter policy) {
161                this.policy = policy;
162            }
163    
164            /**
165             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
166             */
167            public DNSLookupContinuation checkSPF(SPFSession spfData)
168                    throws PermErrorException, TempErrorException,
169                    NeutralException, NoneException {
170                SPF1Record res = (SPF1Record) spfData.getAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD);
171                res = policy.getSPFRecord(spfData.getCurrentDomain(), res);
172                spfData.setAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD, res);
173                return null;
174            }
175            
176            public String toString() {
177                return "PFC:"+policy.toString();
178            }
179    
180        }
181    
182        private DNSService dnsProbe;
183    
184        private SPFRecordParser parser;
185    
186        private Logger log;
187        
188        private String defaultExplanation = null;
189        
190        private boolean useBestGuess = false;
191    
192        private FallbackPolicy fallBack;
193        
194        private OverridePolicy override;
195        
196        private boolean useTrustedForwarder = false;
197        
198        private boolean mustEquals = false;
199    
200        private MacroExpand macroExpand;
201    
202        private SPFExecutor executor;
203    
204        /**
205         * Uses passed logger and passed dnsServicer
206         * 
207         * @param dnsProbe the dns provider
208         * @param logger the logger to use
209         */
210        public SPF(DNSService dnsProbe, Logger logger) {
211            super();
212            this.dnsProbe = dnsProbe;
213            this.log = logger;
214            WiringServiceTable wiringService = new WiringServiceTable();
215            wiringService.put(LogEnabled.class, this.log);
216            wiringService.put(DNSServiceEnabled.class, this.dnsProbe);
217            this.macroExpand = new MacroExpand(logger.getChildLogger("macroExpand"), this.dnsProbe);
218            wiringService.put(MacroExpandEnabled.class, this.macroExpand);
219            this.parser = new RFC4408SPF1Parser(logger.getChildLogger("parser"), new DefaultTermsFactory(logger.getChildLogger("termsfactory"), wiringService));
220            // We add this after the parser creation because services cannot be null
221            wiringService.put(SPFCheckEnabled.class, this);
222            this.executor = new SynchronousSPFExecutor(log, dnsProbe);
223        }
224        
225        
226        /**
227         * Uses passed services
228         * 
229         * @param dnsProbe the dns provider
230         * @param parser the parser to use
231         * @param logger the logger to use
232         */
233        public SPF(DNSService dnsProbe, SPFRecordParser parser, Logger logger, MacroExpand macroExpand, SPFExecutor executor) {
234            super();
235            this.dnsProbe = dnsProbe;
236            this.parser = parser;
237            this.log = logger;
238            this.macroExpand = macroExpand;
239            this.executor = executor;
240        }
241    
242        
243        private static final class DefaultSPFChecker implements SPFChecker, SPFCheckerExceptionCatcher {
244            
245            private Logger log;
246    
247            public DefaultSPFChecker(Logger log) {
248                this.log = log;
249            }
250    
251            /**
252             * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
253             */
254            public DNSLookupContinuation checkSPF(SPFSession spfData)
255                    throws PermErrorException, TempErrorException,
256                    NeutralException, NoneException {
257                if (spfData.getCurrentResultExpanded() == null) {
258                    String resultChar = spfData.getCurrentResult() != null ? spfData.getCurrentResult() : "";
259                    String result = SPF1Utils.resultToName(resultChar);
260                    spfData.setCurrentResultExpanded(result);
261                }
262                return null;
263            }
264            
265    
266            /**
267             * @see org.apache.james.jspf.core.SPFCheckerExceptionCatcher#onException(java.lang.Exception, org.apache.james.jspf.core.SPFSession)
268             */
269            public void onException(Exception exception, SPFSession session)
270                    throws PermErrorException, NoneException, TempErrorException,
271                    NeutralException {
272    
273                String result;
274                if (exception instanceof SPFResultException) {
275                    result = ((SPFResultException) exception).getResult();
276                    if (!SPFErrorConstants.NEUTRAL_CONV.equals(result)) {
277                        log.warn(exception.getMessage(),exception);
278                    }
279                } else {
280                    // this should never happen at all. But anyway we will set the
281                    // result to neutral. Safety first ..
282                    log.error(exception.getMessage(),exception);
283                    result = SPFErrorConstants.NEUTRAL_CONV;
284                }
285                session.setCurrentResultExpanded(result);
286            }
287    
288        }
289        
290        /**
291         * Run check for SPF with the given values.
292         * 
293         * @param ipAddress
294         *            The ipAddress the connection is comming from
295         * @param mailFrom
296         *            The mailFrom which was provided
297         * @param hostName
298         *            The hostname which was provided as HELO/EHLO
299         * @return result The SPFResult
300         */
301        public SPFResult checkSPF(String ipAddress, String mailFrom, String hostName) {
302            SPFSession spfData = null;
303    
304            // Setup the data
305            spfData = new SPFSession(mailFrom, hostName, ipAddress);
306          
307    
308            SPFChecker resultHandler = new DefaultSPFChecker(log);
309            
310            spfData.pushChecker(resultHandler);
311            spfData.pushChecker(this);
312            
313            FutureSPFResult ret = new FutureSPFResult(log);
314            
315            executor.execute(spfData, ret);
316    
317            // if we call ret.getResult it waits the result ;-)
318    //        log.info("[ipAddress=" + ipAddress + "] [mailFrom=" + mailFrom
319    //                + "] [helo=" + hostName + "] => " + ret.getResult());
320    
321            return ret;
322    
323        }
324    
325    
326        /**
327         * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession)
328         */
329        public DNSLookupContinuation checkSPF(SPFSession spfData) throws PermErrorException,
330                NoneException, TempErrorException, NeutralException {
331    
332            // if we already have a result we don't need to add further processing.
333            if (spfData.getCurrentResultExpanded() == null && spfData.getCurrentResult() == null) {
334                SPFChecker policyChecker = new PolicyChecker(getPolicies());
335                SPFChecker recordChecker = new SPFRecordChecker();
336                
337                spfData.pushChecker(recordChecker);
338                spfData.pushChecker(policyChecker);
339            }
340            
341            return null;
342        }
343    
344        /**
345         * Return a default policy for SPF
346         */
347        public LinkedList<SPFChecker> getPolicies() {
348    
349            LinkedList<SPFChecker> policies = new LinkedList<SPFChecker>();
350            
351            if (override != null) {
352                policies.add(new SPFPolicyChecker(override));
353            }
354    
355            policies.add(new InitialChecksPolicy());
356    
357            if (mustEquals) {
358                policies.add(new SPFStrictCheckerRetriever());
359            } else {
360                policies.add(new SPFRetriever());
361            }
362    
363            if (useBestGuess) {
364                policies.add(new SPFPolicyPostFilterChecker(new BestGuessPolicy()));
365            }
366            
367            policies.add(new SPFPolicyPostFilterChecker(new ParseRecordPolicy(parser)));
368            
369            if (fallBack != null) {
370                policies.add(new SPFPolicyPostFilterChecker(fallBack));
371            }
372    
373            policies.add(new SPFPolicyPostFilterChecker(new NoSPFRecordFoundPolicy()));
374            
375            // trustedForwarder support is enabled
376            if (useTrustedForwarder) {
377                policies.add(new SPFPolicyPostFilterChecker(new TrustedForwarderPolicy(log)));
378            }
379    
380            policies.add(new SPFPolicyPostFilterChecker(new NeutralIfNotMatchPolicy()));
381    
382            policies.add(new SPFPolicyPostFilterChecker(new DefaultExplanationPolicy(log, defaultExplanation, macroExpand)));
383            
384            return policies;
385        }
386        
387        /**
388         * Set the amount of time (in seconds) before an TermError is returned when
389         * the dnsserver not answer. Default is 20 seconds.
390         * 
391         * @param timeOut The timout in seconds
392         */
393        public synchronized void setTimeOut(int timeOut) {
394            log.debug("TimeOut was set to: " + timeOut);
395            dnsProbe.setTimeOut(timeOut);
396        }
397        
398        /**
399         * Set the default explanation which will be used if no explanation is found in the SPF Record
400         *  
401         * @param defaultExplanation The explanation to use if no explanation is found in the SPF Record
402         */
403        public synchronized void setDefaultExplanation(String defaultExplanation) {
404            this.defaultExplanation = defaultExplanation;      
405        }
406        
407        /**
408         * Set to true for using best guess. Best guess will set the SPF-Record to "a/24 mx/24 ptr ~all" 
409         * if no SPF-Record was found for the doamin. When this was happen only pass or netural will be returned.
410         * Default is false.
411         * 
412         * @param useBestGuess true to enable best guess
413         */
414        public synchronized void setUseBestGuess(boolean useBestGuess) {
415            this.useBestGuess  = useBestGuess;
416        }
417        
418        
419        /**
420         * Return the FallbackPolicy object which can be used to 
421         * provide default spfRecords for hosts which have no records
422         * 
423         * @return the FallbackPolicy object
424         */
425        public synchronized FallbackPolicy getFallbackPolicy() {
426            // Initialize fallback policy
427            if (fallBack == null) {
428                this.fallBack =  new FallbackPolicy(log.getChildLogger("fallbackpolicy"), parser);
429            }
430            return fallBack;
431        }
432        
433        /**
434         * Set to true to enable trusted-forwarder.org whitelist. The whitelist will only be queried if
435         * the last Mechanism is -all or ?all. 
436         * See http://trusted-forwarder.org for more informations
437         * Default is false.
438         * 
439         * @param useTrustedForwarder true or false
440         */
441        public synchronized void setUseTrustedForwarder(boolean useTrustedForwarder) {
442            this.useTrustedForwarder = useTrustedForwarder;
443        }
444        
445        /**
446         * Return the OverridePolicy object which can be used to
447         * override spfRecords for hosts
448         * 
449         * @return the OverridePolicy object
450         */
451        public synchronized OverridePolicy getOverridePolicy() {
452            if (override == null) {
453                override = new OverridePolicy(log.getChildLogger("overridepolicy"), parser);
454            }
455            return override;
456        }
457        
458        /**
459         * Set to true if a PermError should returned when a domain publish a SPF-Type
460         * and TXT-Type SPF-Record and both are not equals. Defaults false
461         * 
462         * @param mustEquals true or false
463         */
464        public synchronized void setSPFMustEqualsTXT(boolean mustEquals) {
465            this.mustEquals = mustEquals;
466        }
467    
468    
469    }