Postfix provides pluggable access control for incoming messages via its
Access Policy Delegation protocol, but alas, it seems that no one has still dared to make a Java implementation. Such an implementation would open a gateway to many useful and easy integrations, thanks to the great wealth of Java libraries.
So here I present a little trip in this seemingly unexplored field.
As a base framework I'm going to use
Apache Mina, which provides a powerful infrastructure for TCP/IP network applications.
First of all, let's define two classes, one for the request and another for the response (response is so simple that it can be modeled with a Java enum):
package jglatre.postfix.apd;
import java.io.*;
import java.util.Properties;
public class ApdRequest {
private Properties attributes;
public ApdRequest(InputStream in) throws IOException {
attributes = new Properties();
attributes.load(in);
}
public String get(String key) {
return attributes.getProperty(key);
}
}
package jglatre.postfix.apd;
public enum ApdResponse {
dunno,
permit,
defer_if_permit
}
So far, we still haven't seen anything related to Mina. Let's plug those classes in with a
ProtocolCodecFactory implementation:
package jglatre.postfix.apd;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.*;
import org.apache.mina.filter.codec.textline.TextLineEncoder;
public class ApdCodecFactory implements ProtocolCodecFactory {
private static ProtocolDecoder decoder = new ProtocolDecoderAdapter() {
public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
out.write( new ApdRequest(in.asInputStream()) );
}
};
private static ProtocolEncoder encoder = new TextLineEncoder() {
@Override public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
super.encode(session, "action=" + message + "\n", out);
}
};
public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return decoder;
}
public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return encoder;
}
}
With this class we are telling Mina how to transform raw incoming bytes into a request, and a response into raw outgoing bytes. Nothing more and nothing less.
Now we just need a way to tell Mina how to handle a request and get a response. Mina provides a way to plug this kind of functionality: the
IoHandler interface. So we implement this interface (it can be easily done just extending IoHandlerAdapter abstract class):
package jglatre.postfix.apd;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
public abstract class ApdHandler extends IoHandlerAdapter {
public void messageReceived(IoSession session, Object message) throws Exception {
session.write( handleRequest((ApdRequest) message) );
}
protected abstract ApdResponse handleRequest(ApdRequest request);
}
We have defined an abstract handler, so we have a template to create any concrete handler easily. For example, we could implement a "say dunno to all" handler:
class DummyApdHandler extends ApdHandler {
protected ApdResponse handleRequest(ApdRequest request) {
System.out.println( request );
return ApdResponse.dunno;
}
}
But let's create a smarter handler: a grey list. From the handler's point of view a grey list is just a black box which for a given triplet responds "yes" or "no", so we create the abstraction of such a thing (and the triplet class):
package jglatre.postfix.apd.greylist;
public interface Greylist {
boolean accept(Triplet triplet);
}
package jglatre.postfix.apd.greylist;
import jglatre.postfix.apd.ApdRequest;
public class Triplet {
private final String address;
private final String sender;
private final String recipient;
public Triplet(ApdRequest request) {
this(request.get("client_address"),
request.get("sender"),
request.get("recipient"));
}
public Triplet(final String address, final String sender, final String recipient) {
this.address = address;
this.sender = sender;
this.recipient = recipient;
}
public String getAddress() {
return address;
}
public String getSender() {
return sender;
}
public String getRecipient() {
return recipient;
}
}
and the handler which talks to it:
package jglatre.postfix.apd.greylist;
import static jglatre.postfix.apd.ApdResponse.*;
import jglatre.postfix.apd.*;
public class GreylistHandler extends ApdHandler {
private Greylist greylist;
public void setGreylist(Greylist greylist) {
this.greylist = greylist;
}
@Override
protected ApdResponse handleRequest(ApdRequest request) {
Triplet triplet = new Triplet(request);
return greylist.accept(triplet) ? dunno : defer_if_permit;
}
}
Let's implement the grey list class... but, wait a minute, what if we wanted to play around with different kinds of storage before deciding which one performs best? or maybe change it later due to growing needs? the solution, once again, is a template abstract class:
package jglatre.postfix.apd.greylist;
public abstract class AbstractGreylist implements Greylist {
public static enum State {
Unseen,
Blocked,
Ready,
Whitelisted
}
public boolean accept(Triplet triplet) {
switch (getState(triplet)) {
case Unseen:
register(triplet);
case Blocked:
return false;
case Ready:
confirm(triplet);
case Whitelisted:
return true;
default:
throw new RuntimeException("Unknown state");
}
}
protected abstract State getState(Triplet triplet);
protected abstract void register(Triplet triplet);
protected abstract void confirm(Triplet triplet);
}
The concrete implementation of an
InMemoryGreylist is left as exercise to the reader ;-)
Finally let's wire it all together in a server:
package jglatre.postfix.apd;
import java.net.InetSocketAddress;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class ApdServer {
public static void main(String[] args) throws Exception {
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new ApdCodecFactory()));
acceptor.setHandler( new DummyApdHandler() );
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
acceptor.bind( new InetSocketAddress(6666) );
}
}
and tell Postfix about it, adding a line to its main.cf configuration file:
smtpd_client_restrictions = check_policy_service inet:127.0.0.1:6666, permit