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 }