Gumdrop

Gumdrop SMTP Server & Client

Gumdrop provides a complete SMTP implementation for both server and client roles. The server supports RFC 5321 (SMTP) and RFC 6409 (Message Submission) with full ESMTP extensions. Like all Gumdrop protocols, the SMTP implementation is event-driven and non-blocking, achieving high performance with minimal resource consumption.

Contents

Architecture

The SMTP server follows an "event-driven first" design philosophy. Rather than allocating a thread per connection—a model that limits scalability to the thousands—Gumdrop multiplexes all SMTP connections across a small pool of worker threads using Java NIO selectors. This approach has been proven in production since 2005 and enables a single server to handle tens of thousands of concurrent connections.

Protocol State Machine

Each SMTP connection maintains a finite state machine that tracks its progress through the protocol:

State transitions are validated strictly according to RFC 5321, with appropriate error responses for out-of-sequence commands.

Non-Blocking DATA Processing

Message content during the DATA phase is processed as a stream without buffering the entire message in memory. The protocol handles dot-stuffing, CRLF normalisation, and termination detection (CRLF.CRLF) incrementally as data arrives, allowing arbitrarily large messages to be processed efficiently.

Connection Handler

Gumdrop separates protocol mechanics from business logic through the SMTPConnectionHandler interface. The handler receives high-level events about the SMTP session and makes policy decisions without dealing with protocol syntax, response codes, or network I/O.

Handler Interface

Key handler methods:

Asynchronous Callbacks

Policy methods use callbacks rather than return values, enabling asynchronous evaluation. A handler can consult external services—databases, reputation systems, content filters—without blocking the I/O thread. The callback must be invoked on the connection's SelectorLoop thread:

@Override
public void mailFrom(final String sender, final MailFromCallback callback) {
    final SelectorLoop loop = connection.getSelectorLoop();
    
    // Offload reputation check to worker thread
    executor.submit(new Runnable() {
        @Override
        public void run() {
            final SenderPolicyResult result = reputationService.checkSender(sender);
            
            // Re-dispatch callback to the connection's SelectorLoop thread
            loop.invokeLater(new Runnable() {
                @Override
                public void run() {
                    callback.mailFromReply(result);
                }
            });
        }
    });
}

The SMTP connection automatically sends the appropriate protocol response when the callback is invoked. Invoking the callback on the correct thread ensures thread safety for all subsequent protocol operations.

Connection Metadata

Handlers receive rich context through SMTPConnectionMetadata:

Handler Factory

Handlers are created per-connection via a factory, ensuring thread safety and state isolation:

SMTPServer server = new SMTPServer();
server.setHandlerFactory(new SMTPConnectionHandlerFactory() {
    @Override
    public SMTPConnectionHandler createHandler() {
        return new MyMailHandler(config, dependencies);
    }
});

Processing Pipelines

Gumdrop provides a generic SMTPPipeline interface for processing messages as they stream through the server. This enables pluggable processing without modifying core SMTP code.

Pipeline Interface

The pipeline receives notifications at key SMTP stages:

public interface SMTPPipeline {
    void mailFrom(EmailAddress sender);       // MAIL FROM received
    void rcptTo(EmailAddress recipient);      // RCPT TO received
    WritableByteChannel getMessageChannel();  // Called at DATA start
    void endData();                           // Message complete
    void reset();                             // Transaction reset
}

When a pipeline is associated with an SMTPConnection:

This design supports various use cases:

WritableByteChannel Pattern

The WritableByteChannel is a standard Java interface that clearly expresses streaming semantics:

This allows pipelines to process arbitrarily large messages without buffering the entire content in memory.

Email Authentication (SPF/DKIM/DMARC)

The org.bluezoo.gumdrop.smtp.auth package provides AuthPipeline, an implementation of SMTPPipeline that performs SPF, DKIM, and DMARC validation.

How It Works

When associated with a connection, AuthPipeline automatically:

  1. SPF check - runs asynchronously when MAIL FROM is received, using DNS TXT record lookup to validate sender authorization
  2. Header parsing - as message bytes stream in, headers are parsed to collect DKIM-Signature and extract the From domain
  3. Body hashing - raw body bytes (after CRLFCRLF) are hashed for DKIM verification
  4. DKIM verification - at end-of-data, the signature is verified against the DNS public key
  5. DMARC evaluation - after DKIM, DMARC policy is checked with SPF/DKIM alignment

Pipeline Flow Diagram

The following diagram shows how data flows through the AuthPipeline at each stage, including the optional user MessageHandler for receiving teed content:

AuthPipeline Message Flow Diagram
AuthPipeline processes messages in three stages: MAIL FROM (SPF), DATA streaming (header parsing + body hashing), and End of DATA (DKIM + DMARC verification).

Callback-Based Results

Configure the pipeline with callbacks for the checks you care about:

AuthPipeline.Builder builder = new AuthPipeline.Builder(resolver, clientIP, heloHost);

// SPF callback - invoked during MAIL FROM processing
builder.onSPF(new SPFCallback() {
    @Override
    public void onResult(SPFResult result, String explanation) {
        if (result == SPFResult.FAIL) {
            mailFromCallback.mailFromReply(SenderPolicyResult.REJECT);
        } else {
            mailFromCallback.mailFromReply(SenderPolicyResult.ACCEPT);
        }
    }
});

// DKIM callback - invoked at end-of-data
builder.onDKIM(new DKIMCallback() {
    @Override
    public void onResult(DKIMResult result, String domain, String selector) {
        // Store result for logging
    }
});

// DMARC callback - invoked after DKIM
builder.onDMARC(new DMARCCallback() {
    @Override
    public void onResult(DMARCResult result, DMARCPolicy policy, String domain) {
        // Can now respond to DATA command
        if (result == DMARCResult.FAIL && policy == DMARCPolicy.REJECT) {
            dataEndCallback.reply(DataEndReply.REJECT);
        } else {
            dataEndCallback.reply(DataEndReply.ACCEPT);
        }
    }
});

// Or use combined result callback
builder.onComplete(new AuthResultCallback() {
    @Override
    public void onResult(AuthResult result) {
        if (result.shouldReject()) {
            dataEndCallback.reply(DataEndReply.REJECT);
        } else {
            dataEndCallback.reply(DataEndReply.ACCEPT);
        }
    }
});

// Associate with connection
connection.setPipeline(builder.build());

Message Handler Integration

To process message content while authentication runs in parallel, register a MessageHandler:

builder.messageHandler(new MessageHandler() {
    @Override
    public void header(String name, String value) {
        // Receive parsed headers
    }
    
    @Override
    public void addressHeader(String name, List<EmailAddress> addresses) {
        // Receive parsed address headers (From, To, Cc, etc.)
    }
    
    @Override
    public void bodyContent(ByteBuffer data) {
        // Receive decoded body content
    }
});

The handler receives events from a composite handler that also feeds the authentication checks. This avoids parsing the message twice while allowing your application to process content.

AuthResult and Verdict

AuthResult contains individual SPF, DKIM, and DMARC results plus a combined AuthVerdict:

Convenience methods: result.passed(), result.shouldReject(), result.shouldQuarantine().

DKIM Signing (RFC 6376 §5)

The DKIMSigner class generates DKIM-Signature headers for outbound messages. It supports both rsa-sha256 (RFC 6376 §3.3) and ed25519-sha256 (RFC 8463) algorithms, with configurable header and body canonicalization (simple or relaxed).

DKIMSigner signer = new DKIMSigner(privateKey, "example.com", "sel1");
signer.setAlgorithm("rsa-sha256");
signer.setHeaderCanonicalization("relaxed");
signer.setBodyCanonicalization("relaxed");
signer.setSignedHeaders(Arrays.asList("from", "to", "subject", "date"));

// Feed body lines in wire format
signer.bodyLine(bodyBytes, 0, bodyBytes.length);
signer.endBody();

// Sign headers and get the DKIM-Signature header
String dkimHeader = signer.sign(rawHeaderLines);
// Prepend dkimHeader to the message before sending

Ed25519-SHA256 (RFC 8463)

Both the signer and verifier support Ed25519-SHA256 signatures. The verifier correctly handles raw 32-byte public keys in DNS TXT records as specified by RFC 8463 §4, converting them to Java EdECPublicKeySpec for verification. For signing, pass an Ed25519 private key and set the algorithm to "ed25519-sha256".

DMARC Aggregate Reporting (RFC 7489 §7.1)

The DMARCAggregateReport class generates XML aggregate reports conforming to the schema in RFC 7489 Appendix C. Reports can be sent to the domain owner's rua= address (now parsed from DMARC records).

DMARCAggregateReport report = new DMARCAggregateReport();
report.setReporterOrgName("receiver.example.com");
report.setReporterEmail("dmarc-reports@receiver.example.com");
report.setReportId("report-12345");
report.setDateRange(beginEpoch, endEpoch);

// Record results for each evaluated message
report.addResult(sourceIP, headerFrom, policy, adkim, aspf,
    dmarcResult, spfResult, spfDomain, dkimResult, dkimDomain, selector);

// Write the XML report
report.writeXML(outputStream);

The DMARCValidator parses the rua=, ruf=, fo=, and rf= tags from DMARC records. After evaluation, call getLastRua() / getLastRuf() to obtain report destination URIs, and getLastFo() to retrieve the failure reporting options.

DMARC Forensic / Failure Reporting (RFC 7489 §7.2)

The DMARCForensicReport class generates per-message failure reports in the Abuse Reporting Format (ARF, RFC 5965) with DMARC-specific extensions (RFC 6591). Reports are multipart/report MIME messages with three parts: a human-readable description, a machine-readable message/feedback-report, and the original message headers.

// Check if a forensic report should be generated
String fo = validator.getLastFo();
if (DMARCForensicReport.shouldReport(fo, spfResult, dkimResult, dmarcResult)) {
    DMARCForensicReport report = new DMARCForensicReport();
    report.setReporterDomain("receiver.example.com");
    report.setSourceIP(clientIP);
    report.setHeaderFrom(fromDomain);
    report.setDmarcResult(dmarcResult);
    report.setDmarcPolicy(policy);
    report.setSpfResult(spfResult);
    report.setDkimResult(dkimResult);
    report.setOriginalHeaders(messageHeaders);

    report.writeMIME(outputStream, boundary);
}

The fo= tag controls when reports are generated: 0 (default) = all mechanisms fail, 1 = any mechanism fails, d = DKIM fails, s = SPF fails. Multiple options may be colon-separated.

Security Policies

Gumdrop provides comprehensive policy evaluation through typed result enums that abstract SMTP response codes.

Sender Policy Results

SenderPolicyResult for MAIL FROM evaluation:

Recipient Policy Results

RecipientPolicyResult for RCPT TO evaluation:

Connection-Level Filtering

The SMTP server performs connection-level filtering before any protocol exchange:

Blocked connections are rejected immediately without consuming handler resources.

Authentication

Gumdrop supports the full range of SMTP AUTH mechanisms:

All mechanisms are fully implemented on the server side, including multi-round challenge-response exchanges (CRAM-MD5, DIGEST-MD5, SCRAM-SHA-256) and token-based authentication (OAUTHBEARER with JSON error challenges per RFC 7628 §3.2.2). If a client sends * during an AUTH exchange, the server cancels the attempt and responds with 501 (RFC 4954 §4).

Authentication integrates with Gumdrop's centralised org.bluezoo.gumdrop.auth.Realm facility for unified credential management across all servers.

STARTTLS

The server supports opportunistic TLS upgrade via STARTTLS (RFC 3207). When a keystore is configured, STARTTLS is advertised in EHLO responses. After successful TLS negotiation, the SMTP session resets and the client must re-issue EHLO.

SMTP Extensions

Beyond the standard ESMTP capabilities, Gumdrop supports several extensions for enhanced functionality:

CHUNKING (RFC 3030)

The BDAT command provides an alternative to DATA for transmitting message content. Instead of using dot-stuffing and CRLF.CRLF termination, BDAT specifies an exact byte count:

BDAT 1000
<exactly 1000 bytes of content>
BDAT 500 LAST
<exactly 500 bytes of content>

Benefits of CHUNKING:

The handler receives message content through the same messageContent() method regardless of whether DATA or BDAT was used—the protocol difference is handled entirely by SMTPConnection.

BINARYMIME (RFC 3030)

BINARYMIME extends CHUNKING to allow raw binary content without any encoding. The sender declares binary content via the BODY parameter:

MAIL FROM:<sender@example.com> BODY=BINARYMIME

When BODY=BINARYMIME is declared, the message MUST be transmitted using BDAT because the DATA command's dot-stuffing could corrupt binary data. The server enforces this—if a client attempts to use DATA after declaring BINARYMIME, the server rejects with a 503 error.

The BODY parameter supports three values:

The handler does not need to know about BODY type—the difference is entirely a transport concern handled by SMTPConnection.

SMTPUTF8 (RFC 6531)

Internationalized Email allows UTF-8 characters in email addresses, including the local-part, domain, and display names. When a client includes the SMTPUTF8 parameter with MAIL FROM, the server accepts addresses like:

MAIL FROM:<用户@例え.jp> SMTPUTF8
RCPT TO:<почта@пример.рф>

The smtputf8 flag is passed to the handler via the mailFrom() callback, allowing the handler to configure message parsing appropriately. The MessageParser class provides setSmtputf8(boolean) for this purpose.

XCLIENT (Postfix Extension)

XCLIENT allows authorized mail proxies and content filters to pass original client connection information to the MTA. This is essential when mail passes through intermediary systems that need to preserve the original client's identity for logging, policy evaluation, and authentication checks.

Supported XCLIENT attributes:

After XCLIENT, the session resets to the greeting state and the client must re-issue EHLO. The overridden values are reflected in SMTPConnectionMetadata for policy decisions.

XCLIENT is only available to authorized clients. The handler controls authorization through the isXclientAuthorized(InetAddress) method, which returns false by default. Enable it only for trusted hosts:

@Override
public boolean isXclientAuthorized(InetAddress clientAddress) {
    // Allow localhost and internal mail gateways
    return clientAddress.isLoopbackAddress() ||
           clientAddress.getHostAddress().startsWith("10.0.1.");
}

DSN - Delivery Status Notifications (RFC 3461)

DSN allows senders to request notification of message delivery status. The sender can specify when to receive notifications and how much of the original message to include in the notification.

MAIL FROM parameters:

RCPT TO parameters:

Example transaction with DSN:

MAIL FROM:<sender@example.com> RET=HDRS ENVID=msg12345
RCPT TO:<user@example.org> NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;user@example.org

The DSN parameters are accessible through SMTPConnectionMetadata:

The MTA is responsible for generating actual DSN messages based on these parameters when delivery succeeds, fails, or is delayed.

LIMITS (RFC 9422)

The LIMITS extension allows the server to advertise operational limits upfront in the EHLO response. This helps clients avoid sending commands that would be rejected due to limits.

Advertised limits:

Example EHLO response with LIMITS:

250-LIMITS RCPTMAX=100 MAILMAX=50

Configuration via SMTPServer:

When limits are exceeded:

Delivery Requirements

Several SMTP extensions allow senders to specify requirements for how messages should be delivered. These are collected in the DeliveryRequirements interface, which is passed to the handler's mailFrom() callback:

REQUIRETLS (RFC 8689)

Indicates that the message must only be transmitted over TLS-protected connections. If TLS cannot be guaranteed for any hop, the message should bounce.

MAIL FROM:<sender@example.com> REQUIRETLS

The server automatically supports REQUIRETLS when TLS is available. If specified on a non-TLS connection, the server returns 530 5.7.10 REQUIRETLS requires TLS connection.

MT-PRIORITY (RFC 6710)

Specifies message transfer priority from -9 (lowest) to +9 (highest), with 0 being normal priority. MTAs may use this to prioritize queue processing.

MAIL FROM:<sender@example.com> MT-PRIORITY=5
FUTURERELEASE (RFC 4865)

Requests that the message be held for later delivery. Two forms are supported:

MAIL FROM:<sender@example.com> HOLDFOR=3600
MAIL FROM:<sender@example.com> HOLDUNTIL=2025-12-25T00:00:00Z

HOLDFOR specifies seconds from now; HOLDUNTIL specifies an ISO 8601 timestamp.

DELIVERBY (RFC 2852)

Specifies a delivery deadline. If the message cannot be delivered in time, it should be returned (R) or a delay DSN sent (N):

MAIL FROM:<sender@example.com> BY=86400;R
MAIL FROM:<sender@example.com> BY=86400;N
Handler Usage

All delivery requirements are available via DeliveryRequirements:

@Override
public void mailFrom(EmailAddress sender, boolean smtputf8,
                     DeliveryRequirements delivery, MailFromCallback callback) {
    if (delivery.isRequireTls()) {
        // Must relay over TLS only
    }
    if (delivery.hasPriority()) {
        int priority = delivery.getPriority(); // -9 to +9
    }
    if (delivery.isFutureRelease()) {
        Instant releaseTime = delivery.getReleaseTime();
    }
    if (delivery.hasDeliverByDeadline()) {
        Instant deadline = delivery.getDeliverByDeadline();
        boolean returnOnFail = delivery.isDeliverByReturn();
    }
    if (delivery.hasDsnParameters()) {
        DSNReturn ret = delivery.getDsnReturn();
        String envid = delivery.getDsnEnvelopeId();
    }
    callback.mailFromReply(SenderPolicyResult.ACCEPT);
}

Configuration

SMTPService Properties

SMTP services (org.bluezoo.gumdrop.smtp.SMTPService subclasses) support:

SMTPListener Properties

Each <listener> element within the service configures a listener:

Example Configuration

<!-- Authentication realm -->
<realm id="mailRealm" class="org.bluezoo.gumdrop.auth.BasicRealm">
    <property name="href">mail-users.xml</property>
</realm>

<!-- SMTP service with MX and submission listeners -->
<service class="com.example.MyMailService">
    <property name="realm" ref="#mailRealm"/>
    <property name="max-message-size">52428800</property>

    <!-- MX listener (port 25) -->
    <listener class="org.bluezoo.gumdrop.smtp.SMTPListener"
            name="mx" port="25"
            keystore-file="keystore.p12" keystore-pass="secret"
            max-connections-per-ip="20"
            blocked-networks="10.0.0.0/8"/>

    <!-- Submission listener (port 587, auth required) -->
    <listener class="org.bluezoo.gumdrop.smtp.SMTPListener"
            name="submission" port="587"
            keystore-file="keystore.p12" keystore-pass="secret"
            auth-required="true"/>

    <!-- UNIX domain socket for local delivery agents -->
    <listener class="org.bluezoo.gumdrop.smtp.SMTPListener"
            name="local" path="/var/run/smtp.sock"/>
</service>

SMTP Client

Gumdrop includes a non-blocking SMTP client that shares the same event-driven architecture as the server. The client is designed for integration into MTA forward delivery components and service-to-service communication.

SelectorLoop Affinity

When an SMTP client connection is initiated from within a Gumdrop server context—such as a mail relay or gateway—it can be assigned to the same SelectorLoop as the originating server connection. This provides the same benefits described for the HTTP client:

This makes the SMTP client particularly well-suited for MTA implementations where inbound mail must be relayed to downstream servers. The relay operation executes entirely within the I/O worker without blocking.

Client Features

Client Handler Interface

The client is driven by an SMTPClientHandler that receives protocol events:

SMTPClient client = new SMTPClient("mail.example.com", 25);
client.connect(new SMTPClientHandler() {
    @Override
    public void onConnected() {
        // TCP connection established, awaiting greeting
    }
    
    @Override
    public void onGreeting(SMTPResponse greeting, SMTPClientProtocolHandler handler) {
        handler.ehlo("myhost.example.com");
    }
    
    @Override
    public void onReply(SMTPResponse response, SMTPClientProtocolHandler handler) {
        // Handle command responses
        if (response.isSuccess()) {
            // Proceed with next command
        }
    }
    
    @Override
    public void onTLSStarted() {
        // TLS negotiation complete, re-issue EHLO
    }
    
    @Override
    public void onError(SMTPException error) {
        // Handle protocol or connection errors
    }
    
    @Override
    public void onDisconnected() {
        // Connection closed
    }
});

Message Transmission

Message content is sent as a stream with automatic dot-stuffing:

// After MAIL FROM and RCPT TO accepted, DATA command sent
conn.data();  // Sends DATA command

// In onReply(), when 354 received:
ByteBuffer content = getMessageContent();
conn.messageContent(content);  // Dot-stuffed automatically
conn.endData();  // Sends CRLF.CRLF terminator

Telemetry

The SMTP server integrates with Gumdrop's OpenTelemetry implementation for distributed tracing. When telemetry is enabled, the server creates traces and spans that provide visibility into mail flow.

Trace Structure

Session spans capture:

Telemetry data is exported to an OpenTelemetry Collector using the OTLP/HTTP protocol. The exporter uses Gumdrop's native HTTP client with SelectorLoop affinity for efficient, non-blocking delivery.


← Back to Main Page | HTTP Server & Client | IMAP Server | POP3 Server | DNS Server | Telemetry

Gumdrop SMTP Server