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 }