I would like to write a custom check_policy_service (http://www.postfix.org/SMTPD_POLICY_README.html) for postfix with spring-boot.
Short: postfix sends multiple lines like:
foo=bar
me=you
year=123
[empty line]
The empty line indicates, that the request is complete, now the spring-boot app should handle the data and return something like action=ok
Problem:
In my current setup the app tries to handle line 1 foo=bar immediately and does not wait until the [empty line] is sent. How can I make the app wait for the empty line?
Current setup: spring-boot 2.6.3
@Configuration
class TcpServerConfig {
// @Value("\${tcp.server.port}")
private val port = 6676
@Bean
fun serverConnectionFactory(): AbstractServerConnectionFactory {
val serverConnectionFactory = TcpNioServerConnectionFactory(port)
serverConnectionFactory.setUsingDirectBuffers(true)
serverConnectionFactory.isSingleUse = false // reuse socket
return serverConnectionFactory
}
@Bean
fun inboundChannel(): MessageChannel {
return DirectChannel()
}
@Bean
fun inboundGateway(
serverConnectionFactory: AbstractServerConnectionFactory,
inboundChannel: MessageChannel
): TcpInboundGateway {
val tcpInboundGateway = TcpInboundGateway()
tcpInboundGateway.setConnectionFactory(serverConnectionFactory)
tcpInboundGateway.setRequestChannel(inboundChannel)
return tcpInboundGateway
}
}
@MessageEndpoint
class TcpServerEndpoint {
@ServiceActivator(inputChannel = "inboundChannel")
fun process(message: ByteArray): ByteArray {
// I would need to have `message` contain every line until empty line - not a single line
println(String(message))
// TODO handle message accordingly
return "action=ok\n".toByteArray()
}
}
I am not sure where to hook in. Maybe DirectChannel needs to be something else?
I wrote a quick non spring-boot implementation which works, so basically I need the spring-boot-ified version of:
fun main(args: Array<String>) {
val server = ServerSocket(9999)
println("Server is running on port ${server.localPort}")
while (true) {
val client = server.accept()
println("Client connected: ${client.inetAddress.hostAddress}")
// Run client in it's own thread.
thread { ClientHandler(client).run() }
}
}
class ClientHandler(client: Socket) {
private val client: Socket = client
private val reader: BufferedReader = BufferedReader(InputStreamReader(client.getInputStream()))
private val writer: PrintWriter = PrintWriter(client.getOutputStream(), true)
private var running: Boolean = false
fun run() {
running = true
while (running) {
try {
do {
val line = reader.readLine() ?: break
// TODO collect all lines in a list and handle
} while (line.isNotEmpty())
write("action=ok")
} catch (ex: Exception) {
// TODO: Implement exception handling
ex.printStackTrace()
shutdown()
} finally {
}
}
}
private fun write(message: String) {
writer.write((message + "\n\n"))
writer.flush()
}
private fun shutdown() {
running = false
client.close()
println("${client.inetAddress.hostAddress} closed the connection")
}
}
The magic part is read all lines until line is empty and then handle the request(s)