Receive email in real time with Java
Verify and log inbound email in Java using the JDK HTTP server and MailKite SDK. This runnable demo guards JSON field types; add durable storage and deduplication before production acknowledgement.
The runnable source and delivery checks live in the shared receiving-email demo repository.
MailKite is the platform we build. The Java receiving guide includes installation, setup and a downloadable receiver. Live means event-driven delivery after arrival, not a guaranteed SMTP delay.
import com.sun.net.httpserver.HttpServer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.mailkite.MailKite;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class ReceiveEmail {
public static void main(String[] args) throws Exception {
String secret = System.getenv("MAILKITE_WEBHOOK_SECRET");
if (secret == null || secret.isBlank()) throw new IllegalStateException("Set MAILKITE_WEBHOOK_SECRET");
MailKite mk = new MailKite("unused-for-verify");
ObjectMapper json = new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "3000"));
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/hooks/mailkite", ex -> {
int status = 200;
String result = mk.replyOk();
if (!"POST".equals(ex.getRequestMethod())) { status = 405; }
else {
String raw = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
if (!mk.verifyWebhook(ex.getRequestHeaders().getFirst("x-mailkite-signature"), raw, secret)) {
status = 401;
} else {
try {
Object parsed = json.readValue(raw, Object.class);
if (!(parsed instanceof Map<?, ?> event)) { status = 400; }
else {
if (!(event.get("type") instanceof String type) || type.isBlank()) { status = 400; }
else if ("email.received".equals(type)) {
if (!(event.get("id") instanceof String id) || id.isBlank()) { status = 400; }
else { System.out.println("received " + id); }
}
// Demonstration only: persist/enqueue and deduplicate before production acknowledgement.
}
} catch (JsonProcessingException e) { status = 400; }
}
}
if (status != 200) result = "{\"error\":\"invalid request\"}";
byte[] body = result.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().set("Content-Type", "application/json");
ex.sendResponseHeaders(status, body.length);
try (var out = ex.getResponseBody()) { out.write(body); }
});
server.start();
}
}
Add the SDK
For Maven, add the MailKite Java client and Jackson parser to your application’s dependencies:
<dependency>
<groupId>dev.mailkite</groupId>
<artifactId>mailkite</artifactId>
<version>0.19.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.0</version>
</dependency>
Use the version currently published in Maven Central when you implement the tutorial. Store the receiving domain’s webhook secret in an environment variable or a secret manager.
Guard runtime JSON types
The code reads the request stream once and verifies that exact payload. A JSON parser must not run before signature verification. The event schema also makes the sender’s display name and the subject optional, so use null-safe access in a production handler rather than assuming every email has them.
Use JDK 17+ for the pattern-matching guards. Startup rejects a missing or blank secret. Signed malformed JSON, a non-object event, a missing or non-string type, and an email.received event without a nonempty string id return 400. This verification/logging demo does not persist or deduplicate messages; durable acceptance must precede production acknowledgement.
Jackson supplies strict JSON parsing here, with trailing tokens rejected explicitly. The SDK’s minimal Json helper is not a general JSON validator. Keep SDK verification first, then let the parser enforce syntax before the runtime field guards.
Parsing as Object.class leaves the root shape open: an object becomes a map, while an array or scalar is still valid JSON. The instanceof Map<?, ?> event guard rejects those other roots without an unchecked cast. String pattern guards then distinguish a real message id from a number or null. A parser exception becomes 400 instead of escaping the HTTP callback.
Put the handler behind your app’s server
The JDK server is useful for seeing the protocol, but most Java services already run Spring Boot, Micronaut, or another framework. In a framework controller, apply the same sequence: preserve the raw body, verify the header, parse the event, persist it, return the success acknowledgement. Do not put the webhook secret in a checked-in application.properties file.
readAllBytes() buffers the entire request, and this JDK demo does not impose a body-size limit. Apply one at the proxy or in a bounded reader before exposing it publicly. The response length is computed from UTF-8 bytes, not Java character count, and closing the response stream completes the exchange. Keep that lifecycle intact when replacing the logger with storage.
The SDK’s Spring Boot starter is a separate integration for sending mail through the framework’s JavaMailSender abstraction. Receiving is still an HTTP controller concern.
Configure and test
You can also keep an existing Gmail or iCloud mailbox and forward new messages to the receiving address. Use the original provider’s API when mailbox state matters. Postmark inbound is another hosted choice; SES receipt rules or self-hosted Haraka give more control while leaving parsing and delivery operations to you.
Verify the receiving domain, publish the MX record, and point a route at the public HTTPS address for /hooks/mailkite. Use a signed test event first. Confirm that an invalid signature receives 401, signed malformed JSON receives 400, and a valid email.received event is logged. Repeated ids are logged again in this demo. After adding durable storage, test that a repeat creates no second inbox row or side effect.
The Java SDK’s ReceiveWebhook.java is the source example for the verifier and response helper. For the common event schema, see the receiving docs.