blob: bfdf6e35099872eb8acd135bc4a19c91eb0ba6e2 [file] [log] [blame]
/****************************************************************
* 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.smtpserver.fastfail;
import java.util.Optional;
import javax.inject.Inject;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.james.core.MaybeSender;
import org.apache.james.jspf.core.DNSService;
import org.apache.james.jspf.core.exceptions.SPFErrorConstants;
import org.apache.james.jspf.executor.SPFResult;
import org.apache.james.jspf.impl.DefaultSPF;
import org.apache.james.jspf.impl.SPF;
import org.apache.james.protocols.api.ProtocolSession;
import org.apache.james.protocols.api.ProtocolSession.State;
import org.apache.james.protocols.api.handler.ProtocolHandler;
import org.apache.james.protocols.smtp.SMTPRetCode;
import org.apache.james.protocols.smtp.SMTPSession;
import org.apache.james.protocols.smtp.dsn.DSNStatus;
import org.apache.james.protocols.smtp.hook.HookResult;
import org.apache.james.protocols.smtp.hook.HookReturnCode;
import org.apache.james.protocols.smtp.hook.MailHook;
import org.apache.james.smtpserver.JamesMessageHook;
import org.apache.mailet.Attribute;
import org.apache.mailet.AttributeName;
import org.apache.mailet.AttributeValue;
import org.apache.mailet.Mail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SPFHandler implements JamesMessageHook, MailHook, ProtocolHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(SPFHandler.class);
/** This log is the fall back shared by all instances */
private static final Logger FALLBACK_LOG = LoggerFactory.getLogger(SPFHandler.class);
/**
* Non context specific log should only be used when no context specific log
* is available
*/
private final Logger serviceLog = FALLBACK_LOG;
private static final ProtocolSession.AttachmentKey<Boolean> SPF_BLOCKLISTED = ProtocolSession.AttachmentKey.of("SPF_BLOCKLISTED", Boolean.class);
private static final ProtocolSession.AttachmentKey<String> SPF_DETAIL = ProtocolSession.AttachmentKey.of("SPF_DETAIL", String.class);
private static final ProtocolSession.AttachmentKey<Boolean> SPF_TEMPBLOCKLISTED = ProtocolSession.AttachmentKey.of("SPF_TEMPBLOCKLISTED", Boolean.class);
private static final ProtocolSession.AttachmentKey<String> SPF_HEADER = ProtocolSession.AttachmentKey.of("SPF_HEADER", String.class);
private static final AttributeName SPF_HEADER_MAIL_ATTRIBUTE_NAME = AttributeName.of("org.apache.james.spf.header");
/** If set to true the mail will also be rejected on a softfail */
private boolean blockSoftFail = false;
private boolean blockPermError = true;
private SPF spf = new DefaultSPF();
/**
* block the email on a softfail
*
* @param blockSoftFail
* true or false
*/
public void setBlockSoftFail(boolean blockSoftFail) {
this.blockSoftFail = blockSoftFail;
}
/**
* block the email on a permerror
*
* @param blockPermError
* true or false
*/
public void setBlockPermError(boolean blockPermError) {
this.blockPermError = blockPermError;
}
/**
* DNSService to use
*
* @param dnsService
* The DNSService
*/
@Inject
public void setDNSService(DNSService dnsService) {
spf = new SPF(dnsService);
}
/**
* Calls a SPF check
*
* @param session
* SMTP session object
*/
private void doSPFCheck(SMTPSession session, MaybeSender sender) {
Optional<String> heloEhlo = session.getAttachment(SMTPSession.CURRENT_HELO_NAME, State.Connection);
// We have no Sender or HELO/EHLO yet return false
if (sender.isNullSender() || !heloEhlo.isPresent()) {
LOGGER.info("No Sender or HELO/EHLO present");
} else {
String ip = session.getRemoteAddress().getAddress().getHostAddress();
SPFResult result = spf.checkSPF(ip, sender.asString(), heloEhlo.get());
String spfResult = result.getResult();
String explanation = "Blocked - see: " + result.getExplanation();
// Store the header
session.setAttachment(SPF_HEADER, result.getHeaderText(), State.Transaction);
LOGGER.info("Result for {} - {} - {} = {}", ip, sender.asString(), heloEhlo, spfResult);
// Check if we should block!
if ((spfResult.equals(SPFErrorConstants.FAIL_CONV)) || (spfResult.equals(SPFErrorConstants.SOFTFAIL_CONV) && blockSoftFail) || (spfResult.equals(SPFErrorConstants.PERM_ERROR_CONV) && blockPermError)) {
if (spfResult.equals(SPFErrorConstants.PERM_ERROR_CONV)) {
explanation = "Block caused by an invalid SPF record";
}
session.setAttachment(SPF_DETAIL, explanation, State.Transaction);
session.setAttachment(SPF_BLOCKLISTED, true, State.Transaction);
} else if (spfResult.equals(SPFErrorConstants.TEMP_ERROR_CONV)) {
session.setAttachment(SPF_TEMPBLOCKLISTED, true, State.Transaction);
}
}
}
@Override
public HookResult doMail(SMTPSession session, MaybeSender sender) {
if (!session.isRelayingAllowed()) {
doSPFCheck(session, sender);
// Check if session is blocklisted
if (session.getAttachment(SPF_BLOCKLISTED, State.Transaction).isPresent()) {
return HookResult.builder()
.hookReturnCode(HookReturnCode.deny())
.smtpDescription(DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH) + " " + session.getAttachment(SPF_TEMPBLOCKLISTED, State.Transaction).orElse(false))
.build();
} else if (session.getAttachment(SPF_TEMPBLOCKLISTED, State.Transaction).isPresent()) {
return HookResult.builder()
.hookReturnCode(HookReturnCode.denySoft())
.smtpReturnCode(SMTPRetCode.LOCAL_ERROR)
.smtpDescription(DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_DIR_SERVER) + " Temporarily rejected: Problem on SPF lookup")
.build();
}
}
return HookResult.DECLINED;
}
@Override
public HookResult onMessage(SMTPSession session, Mail mail) {
// Store the spf header as attribute for later using (when present)
session.getAttachment(SPF_HEADER, State.Transaction).ifPresent(s ->
mail.setAttribute(new Attribute(SPF_HEADER_MAIL_ATTRIBUTE_NAME, AttributeValue.of(s)))
);
return null;
}
@Override
public void init(Configuration config) throws ConfigurationException {
setBlockSoftFail(config.getBoolean("blockSoftFail", false));
setBlockPermError(config.getBoolean("blockPermError", true));
}
}