Skip to content

Repository files navigation

APACHE v2 License Latest Release Javadocs Codacy

smtp-connection-pool

smtp-connection-pool keeps Jakarta Mail Transport connections open and leases each physical connection exclusively to one caller at a time. It supports lazy or eager allocation, bounded waiting, expiration, and clusters of SMTP servers.

It does not build messages or decide how SMTP works. The selected Jakarta Mail provider still owns authentication, TLS, EHLO, PIPELINING, CHUNKING, response parsing, and the actual send.

Version 4.0.0 expands the project beyond its direct API with optional Jakarta Mail and Camel integrations. Existing users can keep the same org.simplejavamail:smtp-connection-pool dependency and direct API.

Start here: executable demos

The best introduction is the non-published demo project. Every example runs against a real dummy SMTP server on a random port and asserts message delivery, physical connection reuse, and clean shutdown.

Runnable example Integration shown Verified result
DirectPoolDemo Direct leases, release, forced failure, invalidation, and recovery 3 messages over 1 connection; then replacement after a dropped connection
SimpleJavaMailDemo Simple Java Mail as a higher-level library built directly on the pool 3 messages over 1 connection
BatchModuleDemo Standalone Simple Java Mail batch callbacks over caller-created Jakarta Mail messages 3 messages over 1 connection
JakartaMailDemo Plain Jakarta Mail with smtppool 3 messages over 1 connection
SpringDemo Spring JavaMailSenderImpl with smtppool 3 messages over 1 connection
CamelDemo Camel with the separate smtppool: adapter 3 messages over 1 connection

Run the complete executable suite with JDK 21:

mvn -pl smtp-connection-pool-demo -am test

Or run DemoLauncher or any individual demo directly from IntelliJ. The demo is built and tested with the rest of the project, but is deliberately excluded from Maven Central. BatchModuleDemo uses the supported standalone path-2 API published in Simple Java Mail 9.3.0 through #698.

Choose one orchestration owner

Start by deciding whether you need a pool at all. Then choose the one layer that should own it:

Option Best when Orchestration owner Lease handling Pooling/clustering
No pool: withOpenConnection / simple batch One sequential unit of work Application / Simple Java Mail Not applicable No
Simple Java Mail Mailer + batch-module Using EmailBuilder and Mailer Simple Java Mail Automatic Yes
Standalone batch-module facade Creating MimeMessage objects while wanting managed callbacks and futures Batch facade Automatic Yes
Direct smtp-connection-pool Needing exact claim, failure, and shutdown control Application Explicit lease Yes
Jakarta smtppool provider Plain Jakarta Mail or Spring owns Transport calls Provider Mapped to connect / close Yes
Camel smtppool: adapter Camel owns endpoints and component lifecycle Camel adapter / provider Automatic Yes

Exactly one component owns the physical connection pool. Never place batch-module or a direct pool around an smtppool transport.

The complete chooser, ownership model, and examples live in Simple Java Mail's SMTP connection pooling and batch orchestration guide.

The pooled choices reduce to three integration paths, not three abstraction levels inside this repository.

Path Choose it when Who manages the pool Status
1. Use the pool directly Your application or a higher-level library needs clustering, explicit leases, and complete failure/shutdown control. Simple Java Mail itself belongs here. Your application or library Available; explicit SmtpTransportLease is available since 4.0.0
2. Use Simple Java Mail's batch-module directly You create Jakarta Mail messages yourself but want Simple Java Mail's asynchronous batch engine and a safe callback API without adopting EmailBuilder and Mailer. Simple Java Mail's batch API Available since Simple Java Mail 9.3.0 through #698
3. Use it as a Jakarta Mail Transport Plain Jakarta Mail, Spring, or Camel already obtains and closes Transport instances. PooledTransport Available since 4.0.0 through #10

Simple Java Mail stays on path 1 internally. Path 2 is a narrower public API over part of its batch engine. Path 3 presents the pool as a normal Jakarta Mail transport protocol.

The architecture, ownership rules, and five flow diagrams are in PRODUCT-VISION.md.

Artifacts

All three published modules are released together at one version. Maven Central also receives the shared smtp-connection-pool-parent POM required by Maven; it is build metadata, not a fourth application dependency.

Artifact Purpose Java
org.simplejavamail:smtp-connection-pool Direct and clustered pool APIs plus SmtpTransportLease 8+
org.simplejavamail:smtp-connection-pool-jakarta-provider Discoverable smtppool Jakarta Mail provider and Session-scoped lifecycle registry 8+
org.simplejavamail:smtp-connection-pool-camel Optional Camel Mail selection adapter; pooling remains in the provider module 17+ (Camel 4.22)

JPMS module names

Starting with 4.0.1, every published JAR declares a stable Automatic-Module-Name:

Artifact Module name
smtp-connection-pool org.simplejavamail.smtpconnectionpool
smtp-connection-pool-jakarta-provider org.simplejavamail.smtpconnectionpool.jakarta
smtp-connection-pool-camel org.simplejavamail.smtpconnectionpool.camel

The transitive object-pool chain is stable as well: generic-object-pool 2.5.0 declares org.bbottema.genericobjectpool, and clustered-object-pool 4.1.0 declares org.bbottema.clusteredobjectpool. The build inspects every packaged manifest and compiles a real module-path consumer requiring all five names.

The repository also contains smtp-connection-pool-demo. It is an example project tested with every build, not a fourth published module, and is explicitly excluded from Maven Central.

The provider module lets Jakarta Mail, Spring, and Camel obtain pooled Transport instances; it does not speak SMTP itself. Applications still supply Angus Mail or another compatible SMTP Transport provider. Each underlying Transport must represent one reusable physical connection—do not hide a second connection pool beneath this one.

Path 1: use the pool directly

<dependency>
    <groupId>org.simplejavamail</groupId>
    <artifactId>smtp-connection-pool</artifactId>
    <version>4.1.0</version>
</dependency>

Create a Session normally, then claim one exclusive lease. Closing an active lease releases it; invalidate it first when a failure makes the connection uncertain.

SmtpConnectionPool pool = new SmtpConnectionPool(new SmtpClusterConfig<Session>());

try (SmtpTransportLease lease = pool.claimTransport(session)) {
    try {
        Session selectedSession = lease.getSession();
        Transport transport = lease.getTransport();
        transport.sendMessage(message, message.getAllRecipients());
    } catch (MessagingException | RuntimeException failure) {
        lease.invalidate();
        throw failure;
    }
}

// Application shutdown: wait until active leases return and connections close.
pool.shutDown().get();

claimTransport can block and throws InterruptedException. Preserve the thread's interruption policy. The default pool is lazy, has a maximum of four physical connections per Session, and makes an available transport eligible for expiration ten seconds after its last claim.

For partial-recipient failures, an advanced integration may release rather than invalidate only when the delegate is demonstrably still connected. Unknown failures should be treated conservatively.

Stop a job that is waiting for a connection

Use the original claimTransport(session) unless the job needs cancellation or its own acquisition budget. For example, a user stops an export while every SMTP connection is busy: the export should leave the queue without closing another job's connection or shutting down the pool.

ClaimControl control = new ClaimControl();
ClaimOptions options = ClaimOptions.withTimeout(30, TimeUnit.SECONDS)
    .withClaimControl(control);

// On the job's worker:
try (SmtpTransportLease lease = pool.claimTransport(session, options)) {
    // Send using this lease; invalidate it on an uncertain transport failure, as above.
}

// From the job's Stop handler, on another thread:
control.requestCancellation();

Both types come from org.bbottema.genericobjectpool. Creating a control does not request cancellation. A cancelled claim throws CancellationException; budget expiry throws IllegalStateException, matching the original SMTP-pool timeout result. Thread interruption remains InterruptedException. The same options work with clustered keyed and load-balanced claims.

The budget starts before registration and selection, includes waiting and connection preparation, and is capped by the configured cluster timeout. It is cooperative, not a hard socket deadline: a running credential supplier, authenticator, DNS lookup or unsupported provider operation must return before the claim can settle. Late transports are disposed, not handed out. Keep provider connection/read/write timeouts configured as well.

Once a lease is handed out, its acquisition control no longer owns it. Retaining or requesting that control cannot revoke the lease or affect its next borrower. The stopped-job demo runs this scenario against a local SMTP server and then reuses the same connection for a healthy send.

Optional physical abort of an owned lease

Physical abort needs provider support. The pool has no built-in Angus abort adapter. An integration that owns a capable physical provider can supply a TransportCancellationSupport through SmtpClusterConfig.withTransportCancellationSupport(...). Its createAbortAction(transport) is called before connect and returns Optional<Runnable>: empty for unsupported transports, or a quick, thread-safe, latched abort action covering this transport's current and replacement sockets. Configure this before building the pool; it replaces the allocator factory. The caller-owned Session is never modified.

Optional<TransportCancellation> cancellation = lease.getCancellation();
if (cancellation.isPresent() && cancellation.get().request()) {
    // Observe the actual send/connect operation exiting, and then its cleanup:
    lease.getDisposalCompletion().toCompletableFuture().get();
}

An effective request atomically marks the lease unusable before invoking the provider. A racing close() cannot release that connection as healthy, and an old lease's handle cannot abort a new borrower. An unsupported capability is absent, not a successful no-op. This API does not mean a message was unsent: keep the actual sending operation's result, including an accepted final reply that arrived before a late request.

Disposal acknowledgement is separate from operation completion and preserves cleanup failures. It may remain pending after a healthy release until the physical object is eventually retired. Cancelling the returned stage cannot cancel the pool's cleanup. A raw borrowed Transport does not let the pool infer when arbitrary caller code has stopped using it.

Provider / route Cancel pending pool waiting Abort physical connect/send
Angus plain SMTP Yes, with claim options Not built in; observed when the provider returns
Angus STARTTLS or implicit TLS Yes, with claim options Not built in; TLS/socket replacement needs a provider-owned hook
Angus HTTP/SOCKS proxy routing Yes, with claim options Not built in; routing and proxy authentication remain provider-owned
Custom provider with configured cancellation support Yes Only within that provider's documented capability; must cover replacement sockets
Unsupported custom provider Yes No; the optional lease capability is absent
Jakarta smtppool, Spring and Camel integration Existing interruptible waiting No per-send control property; existing provider ownership stays unchanged

See the Angus integration boundary for why Transport.close() is not an abort adapter.

Clustered pools

SmtpClusterConfig<UUID> config = new SmtpClusterConfig<>();
config.getConfigBuilder()
        .defaultCorePoolSize(0)
        .defaultMaxPoolSize(10)
        .loadBalancingStrategy(new RandomAccessLoadBalancing<>())
        .claimTimeout(new Timeout(30, SECONDS));

SmtpConnectionPoolClustered<UUID> pool = new SmtpConnectionPoolClustered<>(config);

ResourceClusterAndPoolKey<UUID, Session> server =
        new ResourceClusterAndPoolKey<>(clusterId, session);
try (SmtpTransportLease lease = pool.claimTransport(server)) {
    lease.getTransport().sendMessage(message, message.getAllRecipients());
}

Clusters and pools are created on demand. Use registerResourceCluster or registerResourcePool when a cluster or server needs different sizing, expiration, or load-balancing behavior.

OAuth2 tokens

The direct allocator resolves a thread-safe token supplier only when it opens or reconnects a physical transport:

session.getProperties().put(
        SmtpConnectionPool.OAUTH2_TOKEN_PROVIDER_PROPERTY,
        (Supplier<String>) tokenProvider::getAccessToken);

The supplier owns caching and refresh. A fixed token remains available through OAUTH2_TOKEN_PROPERTY for short-lived use.

Path 2: Simple Java Mail batch-module

This path is deliberately delivered in Simple Java Mail, not in this repository. Simple Java Mail #698 added a public callback API in Simple Java Mail 9.3.0 for applications that already create Jakarta Mail messages but want asynchronous execution, clustering, and safe release/invalidate handling without adopting the full EmailBuilder/Mailer API.

<dependency>
    <groupId>org.simplejavamail</groupId>
    <artifactId>batch-module</artifactId>
    <version>9.3.0</version>
</dependency>
try (BatchTransportExecutor<String> batch =
             BatchTransportExecutor.<String>builder().build()) {
    batch.registerSession("outbound", session);
    batch.execute("outbound", (selectedSession, transport) -> {
        MimeMessage message = new MimeMessage(selectedSession);
        // populate recipients, subject, and content
        transport.sendMessage(message, message.getAllRecipients());
        return null;
    });
}

BatchTransportExecutor uses SmtpTransportLease internally and remains separate from smtppool. It releases after a successful callback, invalidates after an escaping failure, and owns only the default executor it creates. BatchModuleDemo runs this exact path against a real dummy SMTP server.

Path 3: use it as a Jakarta Mail Transport

Add the provider plus a physical Jakarta Mail implementation:

<dependency>
    <groupId>org.simplejavamail</groupId>
    <artifactId>smtp-connection-pool-jakarta-provider</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>org.eclipse.angus</groupId>
    <artifactId>angus-mail</artifactId>
    <version>2.0.5</version>
</dependency>

The provider registers only smtppool; it never replaces or pretends to be smtp or smtps.

Properties properties = new Properties();
properties.setProperty(SmtpPoolProperties.DELEGATE_PROTOCOL, "smtp"); // default
Session session = Session.getInstance(properties);

Transport transport = session.getTransport("smtppool");
transport.connect(host, port, username, password); // claims and connects if needed
try {
    transport.sendMessage(message, message.getAllRecipients());
} finally {
    transport.close(); // releases a healthy lease; invalidates an unhealthy one
}

Future<?> shutdown = SmtpPoolRegistry.shutdown(session); // graceful
shutdown.get();

Graceful shutdown stops new claims and waits for active leases. If a bounded wait expires, shutdownNow(session) invalidates active leases and returns the same completion handle, which completes only after physical Transport.close() calls finish. A Session can be reused only after that shutdown completes and SmtpPoolRegistry.restart(session) is called explicitly.

When credentials or OAuth tokens rotate for the same endpoint, a new credential-isolated pool becomes current and the superseded pool drains. Its retained credential material is cleared as soon as its last active lease finishes; inactive credential generations do not accumulate.

Spring uses the same provider by configuring JavaMailSenderImpl with protocol smtppool. Camel uses the separate adapter and smtppool: or smtppools: endpoint schemes:

<dependency>
    <groupId>org.simplejavamail</groupId>
    <artifactId>smtp-connection-pool-camel</artifactId>
    <version>4.1.0</version>
</dependency>
to("smtppool://smtp.example.com:587"
        + "?username=user&password=secret&to=recipient@example.com");

See the provider reference and Camel reference for configuration, programmatic delegate selection, credential rotation, and shutdown semantics.

Build and verification

Build the complete project with JDK 21 and Maven. The original pool and Jakarta provider still run on Java 8; the Camel and demo modules require Java 17.

mvn clean verify

Verification runs all module tests, the real-server demo smoke tests, SpotBugs, Javadocs, and a checksum-pinned japicmp comparison with the preceding published 4.0.2 library. CircleCI also compiles and tests the original pool plus Jakarta provider on an actual JDK 8. Its JDK 21 release job versions and publishes all public modules together while leaving out the demo.

Related custom transports

Simple Java Mail #699 may produce a faster physical transport. If it implements Jakarta Mail's synchronous Transport contract with one reusable physical session per instance, it can be selected beneath these paths just like Angus. If it instead uses Simple Java Mail's CustomMailer, it owns its own lifecycle and must not be stacked on this pool.

Current release

4.1.0 (8 September 2026)

  • #31: opt-in acquisition cancellation and total budgets, using clustered-object-pool 4.1.0 and generic-object-pool 2.5.0. Existing calls, Java baselines and provider selection remain supported.
  • Optional provider-neutral physical abort with lease-scoped ownership and separate disposal acknowledgement. Angus physical abort remains unsupported; pending acquisition is independently useful.
  • Failed or cancelled connection preparation closes partially created transports. Cleanup failures remain observable, and the stopped-job demo shows subsequent healthy connection reuse.
  • #27: update the optional Camel adapter to Camel 4.22.0, fixing CVE-2026-59230 in MIME multipart unmarshalling with headersInline=true. The Java 17 baseline is unchanged.

Older releases are recorded in RELEASE.txt.

About

Lightweight SMTP connection pool with clustering support, wait/release mechanism, connection lifecycle management, eager/lazy loading pool with load balancing and auto-expiry policy support

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages